Branch 'dev/boddie' - 75 commits - conf/kolab.conf po/POTFILES.in po/POTFILES.skip po/de.po po/de_DE.po po/pykolab.pot pykolab/Makefile.am pykolab/auth pykolab/cli pykolab/imap pykolab/itip pykolab/translate.py pykolab/wap_client pykolab/xml tests/functional tests/unit wallace/module_invitationpolicy.py wallace/module_resources.py

Paul Boddie boddie at kolabsys.com
Wed Aug 6 17:58:04 CEST 2014


 conf/kolab.conf                                               |    9 
 po/POTFILES.in                                                |   12 
 po/POTFILES.skip                                              |  282 -
 po/de.po                                                      | 2252 +++++++---
 po/de_DE.po                                                   | 2230 +++++++--
 po/pykolab.pot                                                |  917 +++-
 pykolab/Makefile.am                                           |    4 
 pykolab/auth/__init__.py                                      |    5 
 pykolab/auth/ldap/__init__.py                                 |  113 
 pykolab/auth/ldap/cache.py                                    |   35 
 pykolab/cli/cmd_count_domain_mailboxes.py                     |    6 
 pykolab/cli/cmd_delete_mailbox.py                             |    5 
 pykolab/cli/cmd_list_ous.py                                   |   39 
 pykolab/cli/cmd_list_users.py                                 |   39 
 pykolab/cli/cmd_rename_mailbox.py                             |    2 
 pykolab/cli/commands.py                                       |    1 
 pykolab/imap/__init__.py                                      |   15 
 pykolab/imap/cyrus.py                                         |   12 
 pykolab/itip/__init__.py                                      |  287 +
 pykolab/translate.py                                          |   18 
 pykolab/wap_client/__init__.py                                |  287 -
 pykolab/xml/__init__.py                                       |    4 
 pykolab/xml/attendee.py                                       |   81 
 pykolab/xml/contact.py                                        |    4 
 pykolab/xml/contact_reference.py                              |   21 
 pykolab/xml/event.py                                          |  418 +
 pykolab/xml/recurrence_rule.py                                |  118 
 tests/functional/resource_func.py                             |    4 
 tests/functional/test_wallace/test_005_resource_add.py        |   17 
 tests/functional/test_wallace/test_005_resource_invitation.py |  265 -
 tests/functional/test_wallace/test_007_invitationpolicy.py    |  737 +++
 tests/functional/user_add.py                                  |    4 
 tests/unit/test-002-attendee.py                               |   27 
 tests/unit/test-003-event.py                                  |  391 +
 tests/unit/test-011-itip.py                                   |  412 +
 tests/unit/test-011-wallace_resources.py                      |  208 
 tests/unit/test-012-wallace_invitationpolicy.py               |  161 
 tests/unit/test-015-translate.py                              |   23 
 wallace/module_invitationpolicy.py                            | 1015 ++++
 wallace/module_resources.py                                   |  580 +-
 40 files changed, 8796 insertions(+), 2264 deletions(-)

New commits:
commit fee17e6f7a5fa995f7d77c7822e3ed1f2f8bffb5
Merge: e0e89b9 a8555e3
Author: Paul Boddie <paul at boddie.org.uk>
Date:   Wed Aug 6 17:48:26 2014 +0200

    Merge branch 'master' of git://git.kolab.org/git/pykolab into dev/boddie
    
    Conflicts:
    	conf/kolab.conf
    	po/POTFILES.in
    	pykolab/auth/ldap/__init__.py
    	pykolab/auth/ldap/cache.py
    	pykolab/cli/cmd_rename_mailbox.py
    	pykolab/imap/__init__.py
    	pykolab/imap/cyrus.py
    	pykolab/wap_client/__init__.py
    	pykolab/xml/event.py

diff --cc conf/kolab.conf
index 6bf2125,128f0b8..627b23f
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@@ -367,25 -364,14 +371,28 @@@ admin_password = Welcome12
  ; The user canonification result attribute.
  result_attribute = mail
  
 +[dovecot-imap]
 +uri = imaps://localhost:993
 +mail_uid = vmail
 +mail_gid = vmail
 +mail_location = mbox:/var/mail/vmail/%%u
 +
  [wallace]
- modules = resources, footer
+ modules = resources, invitationpolicy, footer
  footer_text = /etc/kolab/footer.text
  footer_html = /etc/kolab/footer.html
  
 +[xmpp]
 +bind_uri =
 +bind_proxy_uri =
 +
 +[smtp]
 +; This will normally be localhost.
 +host =
 +
+ ; default settings for kolabInvitationPolicy
+ kolab_invitation_policy = ACT_ACCEPT_IF_NO_CONFLICT:example.org, ACT_MANUAL
+ 
  ; This is a domain name space specific section, that enables us to override
  ; all settings, for example, the LDAP URI, base and bind DNs, scopes, filters,
  ; etc. Note that overriding the LDAP settings for the primary domain name space
diff --cc pykolab/auth/ldap/__init__.py
index d36f7f6,f15d2c8..5fd337e
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@@ -391,95 -402,40 +391,99 @@@ class LDAP(pykolab.base.Base)
  
          return utils.normalize(_entry_attrs)
  
 -    def find_recipient(self, address="*", exclude_entry_id=None):
 +    def init_entry_attribute(self, entry, attrname):
 +        """
 +            Initialise the attribute with the given attrname in the entry if no
 +            definition is already present.
          """
 -            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.
 +        if not entry.has_key(attrname):
 +            entry[attrname] = self.get_entry_attribute(
 +                    entry['id'],
 +                    attrname
 +                )
  
 -            Specify an additional entry_id to exclude to exclude matches against
 -            the current entry.
 +    def init_folder_acl(self, entry):
 +        """
 +            Initialise the folder ACL entry using the given entry details.
          """
  
 -        self._bind()
 +        folder_path = self.get_folder_path(entry)
 +        folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute')
  
 -        if not exclude_entry_id == None:
 -            __filter_prefix = "(&"
 -            __filter_suffix = "(!(%s=%s)))" % (
 -                    self.config_get('unique_attribute'),
 -                    exclude_entry_id
 -                )
 +        if folderacl_entry_attribute is not None:
 +            self.init_entry_attribute(entry, folderacl_entry_attribute)
 +
 +            if entry[folderacl_entry_attribute] is not None:
 +                # Parse it before assigning it
-                 entry['kolabmailfolderaclentry'] = []
++                entry['kolabfolderaclentry'] = []
 +                if not isinstance(entry[folderacl_entry_attribute], list):
 +                    entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ]
 +
 +                for acl_entry in entry[folderacl_entry_attribute]:
 +                    acl_access = acl_entry.split()[-1]
 +                    aci_subject = ' '.join(acl_entry.split()[:-1])
 +
 +                    log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 +
 +                    access_lookup_dict = {
++                            'all': 'lrsedntxakcpiw',
++                            'append': 'wip',
++                            'full': 'lrswipkxtecdn',
 +                            'read': 'lrs',
++                            'read-only': 'lrs',
++                            'read-write': 'lrswitedn',
 +                            'post': 'p',
-                             'append': 'wip',
++                            'semi-full': 'lrswit',
 +                            'write': 'lrswite',
-                             'all': 'lrsedntxakcpiw'
 +                        }
 +
 +                    if access_lookup_dict.has_key(acl_access):
 +                        acl_access = access_lookup_dict[acl_access]
 +
 +                    log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
  
-                     entry['kolabmailfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
++                    entry['kolabfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
 +
-         self.init_entry_attribute(entry, 'kolabmailfolderaclentry')
++        self.init_entry_attribute(entry, 'kolabfolderaclentry')
 +
 +    def get_folder_path(self, entry):
 +        """
 +            Return the folder path indicated by the given entry or from the
 +            configuration.
 +        """
 +
 +        if entry.get('kolabtargetfolder'):
 +            return entry['kolabtargetfolder']
          else:
 -            __filter_prefix = ""
 -            __filter_suffix = ""
 +            # 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 at domain
 +            result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 +            if result_attribute in ['mail']:
 +                return "%s@%s" % (entry['cn'], self.domain)
 +            else:
 +                return entry['cn']
 +
 +    def get_search_attributes(self):
 +        """
 +            Return search attributes and result attributes.
 +        """
  
 -        kolab_filter = self._kolab_filter()
          recipient_address_attrs = self.config_get_list("mail_attributes")
  
 -        result_attributes = []
 +        result_attributes = recipient_address_attrs[:]
 +        result_attributes.append(self.config_get('unique_attribute'))
  
 -        for recipient_address_attr in recipient_address_attrs:
 -            result_attributes.append(recipient_address_attr)
 +        return recipient_address_attrs, result_attributes
  
 -        result_attributes.append(self.config_get('unique_attribute'))
 +    def get_filter_for_addresses(self, address, attrs, prefix, suffix):
 +        """
 +            Return the filter string for addresses provided by the given
 +            attributes, using the specified prefix and suffix.
 +        """
  
          _filter = "(|"
  
@@@ -1187,7 -1147,7 +1191,7 @@@
          """
          pass
  
--    def _change_add_sharedfolder(self, entry, change):
++    def _change_add_sharedfolder(self, entry, change, modify=False):
          """
              An entry of type sharedfolder was added.
          """
@@@ -1235,16 -1265,21 +1239,20 @@@
                      folder_path,
                      entry['kolabfoldertype']
                  )
++        elif modify:
++            self.imap.set_acl(folder_path, 'anyone', '')
  
-         if entry.get('kolabmailfolderaclentry'):
 -        if entry.has_key('kolabfolderaclentry') and \
 -                not entry['kolabfolderaclentry'] == None:
 -
++        if entry.get('kolabfolderaclentry'):
              self.imap._set_kolab_mailfolder_acls(
-                     entry['kolabmailfolderaclentry']
+                     entry['kolabfolderaclentry']
                  )
+         else:
+             self.imap.set_acl(folder_path, 'anyone', '')
  
 -        if entry.has_key(delivery_address_attribute) and \
 -                not entry[delivery_address_attribute] == None:
 +        if entry.get(delivery_address_attribute):
              self.imap.set_acl(folder_path, 'anyone', '+p')
  
 -        #if server == None:
 +        #if server is None:
              #self.entry_set_attribute(mailserver_attribute, server)
  
      def _change_add_unknown(self, entry, change):
@@@ -1467,8 -1528,137 +1475,8 @@@
      def _change_modify_role(self, entry, change):
          pass
  
-     # A shared folder was modified.
-     _change_modify_sharedfolder = _change_add_sharedfolder
+     def _change_modify_sharedfolder(self, entry, change):
 -        """
 -            A shared folder was modified.
 -        """
 -        self.imap.connect(domain=self.domain)
 -
 -        server = None
 -
 -        # Get some configuration values
 -        mailserver_attribute = self.config_get('mailserver_attribute')
 -        if entry.has_key(mailserver_attribute):
 -            server = entry[mailserver_attribute]
 -
 -        foldertype_attribute = self.config_get('sharedfolder_type_attribute')
 -        if not foldertype_attribute == None:
 -            if not entry.has_key(foldertype_attribute):
 -                entry[foldertype_attribute] = self.get_entry_attribute(
 -                        entry['id'],
 -                        foldertype_attribute
 -                    )
 -
 -            if not entry[foldertype_attribute] == None:
 -                entry['kolabfoldertype'] = entry[foldertype_attribute]
 -
 -        if not entry.has_key('kolabfoldertype'):
 -            entry['kolabfoldertype'] = self.get_entry_attribute(
 -                    entry['id'],
 -                    'kolabfoldertype'
 -                )
 -
 -        # A delivery address is postuser+targetfolder
 -        delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute')
 -        if not delivery_address_attribute == None:
 -            if not entry.has_key(delivery_address_attribute):
 -                entry[delivery_address_attribute] = self.get_entry_attribute(
 -                        entry['id'],
 -                        delivery_address_attribute
 -                    )
 -
 -            if not entry[delivery_address_attribute] == None:
 -                if len(entry[delivery_address_attribute].split('+')) > 1:
 -                    entry['kolabtargetfolder'] = entry[delivery_address_attribute].split('+')[1]
 -
 -        if not entry.has_key('kolabtargetfolder'):
 -            entry['kolabtargetfolder'] = self.get_entry_attribute(
 -                    entry['id'],
 -                    'kolabtargetfolder'
 -                )
 -
 -        if entry.has_key('kolabtargetfolder') and \
 -                not entry['kolabtargetfolder'] == None:
 -
 -            folder_path = entry['kolabtargetfolder']
 -        else:
 -            # TODO: What is *the* way to see if we need to create an @domain
 -            # shared mailbox?
 -            # TODO^2: self.domain, really? Presumes any mail attribute is
 -            # set to the primary domain name space...
 -            # TODO^3: Test if the cn is already something at 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']
 -
 -        folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute')
 -        if folderacl_entry_attribute == None:
 -            folderacl_entry_attribute = 'acl'
 -
 -        if not entry.has_key(folderacl_entry_attribute):
 -            entry[folderacl_entry_attribute] = self.get_entry_attribute(
 -                    entry['id'],
 -                    folderacl_entry_attribute
 -                )
 -
 -        if not entry[folderacl_entry_attribute] == None:
 -            # Parse it before assigning it
 -            entry['kolabfolderaclentry'] = []
 -            if not isinstance(entry[folderacl_entry_attribute], list):
 -                entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ]
 -
 -            for acl_entry in entry[folderacl_entry_attribute]:
 -                acl_access = acl_entry.split()[-1]
 -                aci_subject = ' '.join(acl_entry.split()[:-1])
 -
 -                log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 -
 -                access_lookup_dict = {
 -                        'all': 'lrsedntxakcpiw',
 -                        'append': 'wip',
 -                        'full': 'lrswipkxtecdn',
 -                        'read': 'lrs',
 -                        'read-only': 'lrs',
 -                        'read-write': 'lrswitedn',
 -                        'post': 'p',
 -                        'semi-full': 'lrswit',
 -                        'write': 'lrswite',
 -                    }
 -
 -                if access_lookup_dict.has_key(acl_access):
 -                    acl_access = access_lookup_dict[acl_access]
 -
 -                log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 -
 -                entry['kolabfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
 -
 -        if not self.imap.shared_folder_exists(folder_path):
 -            self.imap.shared_folder_create(folder_path, server)
 -
 -        if entry.has_key('kolabfoldertype') and \
 -                not entry['kolabfoldertype'] == None:
 -
 -            self.imap.shared_folder_set_type(
 -                    folder_path,
 -                    entry['kolabfoldertype']
 -                )
 -        else:
 -            self.imap.set_acl(folder_path, 'anyone', '')
 -
 -        if entry.has_key('kolabfolderaclentry') and \
 -                not entry['kolabfolderaclentry'] == None:
 -
 -            self.imap._set_kolab_mailfolder_acls(
 -                    entry['kolabfolderaclentry']
 -                )
 -        else:
 -            self.imap.set_acl(folder_path, 'anyone', '')
 -
 -        if entry.has_key(delivery_address_attribute) and \
 -                not entry[delivery_address_attribute] == None:
 -            self.imap.set_acl(folder_path, 'anyone', 'p')
++        self._change_add_sharedfolder(entry, change, modify=True)
  
      def _change_modify_user(self, entry, change):
          """
@@@ -1571,13 -1761,46 +1579,24 @@@
          if entry.has_key(mailserver_attribute):
              server = entry[mailserver_attribute]
  
 -        if not entry.has_key('kolabtargetfolder'):
 -            entry['kolabtargetfolder'] = self.get_entry_attribute(
 -                    entry['id'],
 -                    'kolabtargetfolder'
 -                )
 +        self.init_entry_attribute(entry, 'kolabtargetfolder')
  
 -        if not entry.has_key('kolabfoldertype'):
 -            entry['kolabfoldertype'] = self.get_entry_attribute(
 -                    entry['id'],
 -                    'kolabfoldertype'
 -                )
 +        self.init_entry_attribute(entry, 'kolabfoldertype')
  
-         #self.init_entry_attribute(entry, 'kolabmailfolderaclentry')
+         folderacl_entry_attribute = conf.get('ldap', 'folderacl_entry_attribute')
 -        if folderacl_entry_attribute == None:
++        if folderacl_entry_attribute is None:
+             folderacl_entry_attribute = 'acl'
+ 
+         if not entry.has_key(folderacl_entry_attribute):
+             entry['kolabfolderaclentry'] = self.get_entry_attribute(
+                     entry['id'],
+                     folderacl_entry_attribute
+                 )
+         else:
+             entry['kolabfolderaclentry'] = entry[folderacl_entry_attribute]
+             del entry[folderacl_entry_attribute]
  
 -        if entry.has_key('kolabtargetfolder') and \
 -                not entry['kolabtargetfolder'] == None:
 -
 -            folder_path = entry['kolabtargetfolder']
 -        else:
 -            # TODO: What is *the* way to see if we need to create an @domain
 -            # shared mailbox?
 -            # TODO^2: self.domain, really? Presumes any mail attribute is
 -            # set to the primary domain name space...
 -            # TODO^3: Test if the cn is already something at 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']
 +        folder_path = self.get_folder_path(entry)
  
          if not self.imap.shared_folder_exists(folder_path):
              self.imap.shared_folder_create(folder_path, server)
@@@ -1588,16 -1813,40 +1607,38 @@@
                      entry['kolabfoldertype']
                  )
  
-         if entry.get('kolabmailfolderaclentry'):
 -        if entry.has_key('kolabfolderaclentry') and \
 -                not entry['kolabfolderaclentry'] == None:
++        if entry.get('kolabfolderaclentry'):
+ 
+             if isinstance(entry['kolabfolderaclentry'], basestring):
+                 entry['kolabfolderaclentry'] = [ entry['kolabfolderaclentry'] ]
+ 
+             import copy
+             _acls = copy.deepcopy(entry['kolabfolderaclentry'])
+             entry['kolabfolderaclentry'] = []
+ 
+             for _entry in _acls:
+                 if _entry[0] == "(":
+                     entry['kolabfolderaclentry'].append(_entry)
+                     continue
+ 
+                 s,r = [x.strip() for x in _entry.split(',')]
+ 
+                 entry['kolabfolderaclentry'].append("('%s', '%s', '%s')" % (folder_path, s, r))
+ 
              self.imap._set_kolab_mailfolder_acls(
-                     entry['kolabmailfolderaclentry']
+                     entry['kolabfolderaclentry']
                  )
  
+         elif entry['kolabfolderaclentry'] in [None,[]]:
+             for ace in self.imap.list_acls(folder_path):
+                 aci_subject = ace.split()[0]
+                 self.imap.set_acl(folder_path, aci_subject, '')
+ 
          delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute')
 -        if entry.has_key(delivery_address_attribute) and \
 -                not entry[delivery_address_attribute] == None:
 +        if entry.get(delivery_address_attribute):
              self.imap.set_acl(folder_path, 'anyone', '+p')
  
 -        #if server == None:
 +        #if server is None:
              #self.entry_set_attribute(mailserver_attribute, server)
  
      def _change_none_user(self, entry, change):
@@@ -1766,16 -2016,17 +1807,17 @@@
                  else:
                      return _type
  
-     def _find_user_dn(self, login, realm):
+     def _find_user_dn(self, login, kolabuser=False):
          """
-             Find the distinguished name (DN) for an entry in LDAP.
+             Find the distinguished name (DN) for a (Kolab) user entry in LDAP.
          """
  
+         conf_prefix = 'kolab_' if kolabuser else ''
          domain_root_dn = self._kolab_domain_root_dn(self.domain)
  
-         base_dn = self.config_get('user_base_dn')
-         if base_dn is None:
-             base_dn = self.config_get('base_dn')
+         user_base_dn = self.config_get(conf_prefix + 'user_base_dn')
 -        if user_base_dn == None:
++        if user_base_dn is None:
+             user_base_dn = self.config_get('base_dn')
  
          auth_attrs = self.config_get_list('auth_attributes')
  
diff --cc pykolab/auth/ldap/cache.py
index 292bc52,07c3d28..5dcbb76
--- a/pykolab/auth/ldap/cache.py
+++ b/pykolab/auth/ldap/cache.py
@@@ -54,8 -63,8 +54,8 @@@ class Entry(object)
          self.uniqueid = uniqueid
          self.result_attribute = result_attr
  
-         modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format')
+         modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
 -        if modifytimestamp_format == None:
 +        if modifytimestamp_format is None:
              modifytimestamp_format = "%Y%m%d%H%M%SZ"
  
          self.last_change = datetime.datetime.strptime(
@@@ -130,11 -139,11 +130,11 @@@ def get_entry(domain, entry, update=Tru
          db.commit()
          _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
      else:
-         modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format')
+         modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
 -        if modifytimestamp_format == None:
 +        if modifytimestamp_format is None:
              modifytimestamp_format = "%Y%m%d%H%M%SZ"
  
 -        if not _entry.last_change.strftime(modifytimestamp_format) == entry['modifytimestamp']:
 +        if _entry.last_change.strftime(modifytimestamp_format) != entry['modifytimestamp']:
              log.debug(_("Updating timestamp for cache entry %r") % (entry['id']), level=8)
              last_change = datetime.datetime.strptime(entry['modifytimestamp'], modifytimestamp_format)
              _entry.last_change = last_change
@@@ -176,14 -189,17 +180,17 @@@ def init_db(domain,reinit=False)
      return db
  
  def last_modify_timestamp(domain):
-     db = init_db(domain)
-     last_change = db.query(Entry).order_by(desc(Entry.last_change)).first()
- 
-     modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format')
+     modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
 -    if modifytimestamp_format == None:
 +    if modifytimestamp_format is None:
          modifytimestamp_format = "%Y%m%d%H%M%SZ"
  
-     if last_change is not None:
-         return last_change.last_change.strftime(modifytimestamp_format)
- 
-     return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format)
+     try:
+         db = init_db(domain)
+         last_change = db.query(Entry).order_by(desc(Entry.last_change)).first()
+ 
 -        if not last_change == None:
++        if last_change is not None:
+             return last_change.last_change.strftime(modifytimestamp_format)
+         else:
+             return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format)
+     except:
+         return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format)
diff --cc pykolab/cli/cmd_rename_mailbox.py
index f7cf1c1,4efed0d..dfb759d
--- a/pykolab/cli/cmd_rename_mailbox.py
+++ b/pykolab/cli/cmd_rename_mailbox.py
@@@ -63,11 -63,11 +63,11 @@@ def execute(*args, **kw)
      imap.connect(domain=domain)
  
      if not imap.has_folder(source_folder):
 -        print >> sys.stderr, _("Source folder %r does not exist") % (source_folder)
 +        utils.message(_("Source folder %r does not exist") % (source_folder))
          sys.exit(1)
  
-     if imap.has_folder(target_folder):
 -    if imap.has_folder(target_folder) and partition == None:
 -        print >> sys.stderr, _("Target folder %r already exists") % (target_folder)
++    if imap.has_folder(target_folder) and partition is None:
 +        utils.message(_("Target folder %r already exists") % (target_folder))
          sys.exit(1)
  
      imap.user_mailbox_rename(source_folder.replace('user/',''), target_folder.replace('user/',''), partition=partition)
diff --cc pykolab/cli/commands.py
index 82241a4,515d40c..b6dff3a
--- a/pykolab/cli/commands.py
+++ b/pykolab/cli/commands.py
@@@ -29,13 -27,34 +29,12 @@@ from pykolab import util
  conf = pykolab.getConf()
  
  commands = {}
 -command_groups = {}
  
  def __init__():
 -    # We only want the base path
 -    commands_base_path = os.path.dirname(__file__)
 +    import_commands(os.path.dirname(__file__), "pykolab.cli", "cmd_", commands)
  
 -    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, errmsg:
 -                    pass
 -
 -                exec("%s_register()" % (cmd_name))
 -
 -        for dirname in dirnames:
 -            register_group(commands_path, dirname)
 -
 -    register('help', list_commands)
 +    register('help', lambda *args, **kw: list_commands(commands, *args, **kw))
  
-     register('list_users', not_yet_implemented, description="Not yet implemented")
      register('delete_user', not_yet_implemented, description="Not yet implemented")
  
      register('list_groups', not_yet_implemented, description="Not yet implemented")
diff --cc pykolab/imap/__init__.py
index 2115bdf,f52dc9f..be71fd5
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@@ -210,9 -210,8 +210,8 @@@ class IMAP(object)
      def create_folder(self, folder_path, server=None, partition=None):
          folder_path = self.folder_utf7(folder_path)
  
 -        if not server == None:
 +        if server is not None:
-             if not self._imap.has_key(server):
-                 self.connect(server=server)
+             self.connect(server=server)
  
              try:
                  self._imap[server].cm(folder_path, partition=partition)
diff --cc pykolab/wap_client/__init__.py
index 38a5381,9549fe8..7b2b565
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@@ -274,13 -303,21 +303,21 @@@ def group_form_value_generate_mail(para
  
      return request('POST', 'group_form_value.generate_mail', params)
  
- def group_info():
-     group = utils.ask_question("Group email address")
-     group = request('GET', 'group.info?group=%s' % (group))
-     return group
+ 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:
 +    if group is None:
          group = utils.ask_question("Group email address")
      group = request('GET', 'group.members_list?group=%s' % (group))
      return group
@@@ -312,9 -372,7 +372,7 @@@ def request_raw(method, api_uri, get=No
      if conf.debuglevel > 8:
          conn.set_debuglevel(9)
  
-     conn.set_debuglevel(9)
- 
 -    if not get == None:
 +    if get is not None:
          _get = "?%s" % (urllib.urlencode(get))
      else:
          _get = ""
@@@ -337,9 -395,111 +395,111 @@@
  
      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:
++    if params is 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:
++    if params is 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:
++    if params is 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():
      return request('GET', 'system.capabilities')
  
@@@ -425,85 -585,8 +585,8 @@@ def user_form_value_generate(params=Non
  
      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 role_find_by_attribute(params=None):
-     if params is 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_add(params=None):
-     if params is None:
-         role_name = utils.ask_question("Role name")
-         params = {
-                 'cn': role_name
-             }
- 
-     params = json.dumps(params)
- 
-     return request('POST', 'role.add', params)
- 
- def role_delete(params=None):
-     if params is 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_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 user_form_value_generate_uid(params=None):
 -    if params == None:
 +    if params is None:
          params = get_user_input()
  
      params = json.dumps(params)
diff --cc pykolab/xml/contact_reference.py
index 87d7957,5a832da..022e2e6
--- a/pykolab/xml/contact_reference.py
+++ b/pykolab/xml/contact_reference.py
@@@ -11,9 -11,18 +11,18 @@@ import kolabforma
  """
  
  class ContactReference(kolabformat.ContactReference):
+     properties_map = {
+         'email': 'email',
+         'name':  'name',
+         'type':  'type',
+         'uid':   'uid',
+     }
+ 
      def __init__(self, email=None):
 -        if email == None:
 +        if email is None:
              kolabformat.ContactReference.__init__(self)
+         elif isinstance(email, kolabformat.ContactReference):
+             kolabformat.ContactReference.__init__(self, email.email(), email.name(), email.uid())
          else:
              kolabformat.ContactReference.__init__(self, email)
  
diff --cc pykolab/xml/event.py
index 14128e2,72cbfeb..83035d6
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@@ -95,31 -169,37 +169,37 @@@ class Event(object)
          # Required
          event['uid'] = self.get_uid()
  
 -        # NOTE: Make sure to list(set()) or duplicates may arise
 -        for attr in list(set(event.singletons)):
 +        # NOTE: Make sure to set() or duplicates may arise
 +        for attr in set(event.singletons):
-             if hasattr(self, 'get_ical_%s' % (attr.lower())):
-                 retval = getattr(self, "get_ical_%s" % attr.lower())()
+             ical_getter = 'get_ical_%s' % (attr.lower())
+             default_getter = 'get_%s' % (attr.lower())
+             retval = None
+             if hasattr(self, ical_getter):
+                 retval = getattr(self, ical_getter)()
 -                if not retval == None and not retval == "":
 +                if retval:
                      event.add(attr.lower(), retval)
- 
-             elif hasattr(self, 'get_%s' % (attr.lower())):
-                 retval = getattr(self, "get_%s" % attr.lower())()
+             elif hasattr(self, default_getter):
+                 retval = getattr(self, default_getter)()
 -                if not retval == None and not retval == "":
 +                if 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)):
 +        # NOTE: Make sure to set() or duplicates may arise
 +        for attr in set(event.multiple):
-             if hasattr(self, 'get_ical_%s' % (attr.lower())):
-                 retval = getattr(self, "get_ical_%s" % attr.lower())()
-                 if isinstance(retval, list) and retval:
-                     for _retval in retval:
-                         event.add(attr.lower(), _retval, encode=0)
- 
-             elif hasattr(self, 'get_%s' % (attr.lower())):
-                 retval = getattr(self, "get_%s" % attr.lower())()
-                 if isinstance(retval, list) and retval:
-                     for _retval in retval:
-                         event.add(attr.lower(), _retval, encode=0)
+             ical_getter = 'get_ical_%s' % (attr.lower())
+             default_getter = 'get_%s' % (attr.lower())
+             retval = None
+             if hasattr(self, ical_getter):
+                 retval = getattr(self, ical_getter)()
+             elif hasattr(self, default_getter):
+                 retval = getattr(self, default_getter)()
+ 
 -            if isinstance(retval, list) and not len(retval) == 0:
++            if isinstance(retval, list) and retval:
+                 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)
  
          cal.add_component(event)
  
@@@ -167,9 -246,16 +246,16 @@@
          elif hasattr(icalendar.Event, 'from_string'):
              ical_event = icalendar.Event.from_string(ical)
  
+         # 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']:
+             self._xml_from_ical(ical)
+         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)):
 +        for attr in set(ical_event.required):
              if ical_event.has_key(attr):
                  self.set_from_ical(attr.lower(), ical_event[attr])
  
@@@ -442,10 -594,15 +594,15 @@@
          self.event.setAttendees(self._attendees)
  
      def set_classification(self, classification):
-         self.event.setClassification(classification)
+         if classification in self.classification_map.keys():
+             self.event.setClassification(self.classification_map[classification])
+         elif classification in self.classification_map.values():
+             self.event.setClassification(status)
+         else:
+             raise ValueError, _("Invalid classification %r") % (classification)
  
      def set_created(self, _datetime=None):
 -        if _datetime == None:
 +        if _datetime is None:
              _datetime = datetime.datetime.now()
  
          self.event.setCreated(xmlutils.to_cdatetime(_datetime, False))


commit a8555e3e8789fd02b7d5749ea4fc51b84e57285f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 5 00:53:26 2014 -0400

    Improve resource confirmation workflow:
    - Use base64 encoding for original event UIDs
    - Compare sequence number on resource owner replies
    - Added confirmation test scenario with reservation update and outdated replies

diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index c21f12c..61b9402 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -239,11 +239,12 @@ class TestResourceInvitation(unittest.TestCase):
         smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload))
         smtp.quit()
 
-    def send_itip_invitation(self, resource_email, start=None, allday=False, template=None):
+    def send_itip_invitation(self, resource_email, start=None, allday=False, template=None, uid=None):
         if start is None:
             start = datetime.datetime.now()
 
-        uid = str(uuid.uuid4())
+        if uid is None:
+            uid = str(uuid.uuid4())
 
         if allday:
             default_template = itip_allday
@@ -393,6 +394,7 @@ class TestResourceInvitation(unittest.TestCase):
         self.assertEqual(event.get_summary(), "test")
 
 
+    # @depends test_002_invite_resource
     def test_003_invite_resource_conflict(self):
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0))
 
@@ -670,6 +672,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_status(True), 'CONFIRMED')
         self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
 
 
@@ -705,6 +708,74 @@ class TestResourceInvitation(unittest.TestCase):
         response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.room3['mail'])
         self.assertIsInstance(response, email.message.Message)
 
-        # tentative reservation was removed from resource calendar
+        # tentative reservation was set to cancelled
         event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
         self.assertEqual(event, None)
+        #self.assertEqual(event.get_status(True), 'CANCELLED')
+        #self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'DECLINED')
+
+
+    def test_015_owner_confirmation_update(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,8,19, 9,0,0), uid="http://a-totally.stupid/?uid")
+
+        # requester (john) gets a TENTATIVE confirmation
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        # check first confirmation message sent to resource owner (jane)
+        notify1 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notify1, email.message.Message)
+
+        itip_event1 = events_from_message(notify1)[0]
+        self.assertEqual(itip_event1['start'].hour, 9)
+
+        self.purge_mailbox(self.jane['mailbox'])
+        self.purge_mailbox(self.john['mailbox'])
+
+        # send update with new date (and sequence)
+        self.send_itip_update(self.room3['mail'], uid, datetime.datetime(2014,8,19, 16,0,0))
+
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE')
+
+        # check second confirmation message sent to resource owner (jane)
+        notify2 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notify2, email.message.Message)
+
+        itip_event2 = events_from_message(notify2)[0]
+        self.assertEqual(itip_event2['start'].hour, 16)
+
+        # resource owner declines the first reservation request
+        itip_reply = itip_event1['xml'].to_message_itip(self.jane['mail'],
+            method="REPLY",
+            participant_status='DECLINED',
+            message_text="Request declined",
+            subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED'))
+        )
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(self.jane['mail'], str(itip_event1['organizer']), str(itip_reply))
+        smtp.quit()
+
+        time.sleep(5)
+
+        # resource owner accpets the second reservation request
+        itip_reply = itip_event2['xml'].to_message_itip(self.jane['mail'],
+            method="REPLY",
+            participant_status='ACCEPTED',
+            message_text="Request accepred",
+            subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED'))
+        )
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(self.jane['mail'], str(itip_event2['organizer']), str(itip_reply))
+        smtp.quit()
+
+        # requester (john) now gets the ACCEPTED response
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 1af22ff..9bc808f 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -598,7 +598,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "cancelled")
-        self.assertEqual(event.get_status(), 'CANCELLED')
+        self.assertEqual(event.get_status(True), 'CANCELLED')
         self.assertTrue(event.get_transparency())
 
 
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f7c9441..47259da 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -25,7 +25,7 @@ import random
 import tempfile
 import time
 from urlparse import urlparse
-import urllib
+import base64
 import uuid
 import re
 
@@ -204,9 +204,10 @@ def execute(*args, **kw):
 
         for recipient in recipients:
             # extract reference UID from recipients like resource+UID at domain.org
-            if re.match('.+\+[A-Za-z0-9%/_-]+@', recipient):
+            if re.match('.+\+[A-Za-z0-9=/-]+@', recipient):
                 (prefix, host) = recipient.split('@')
-                (local, reference_uid) = prefix.split('+')
+                (local, uid) = prefix.split('+')
+                reference_uid = base64.b64decode(uid, '-/')
                 recipient = local + '@' + host
 
             if not len(resource_record_from_email_address(recipient)) == 0:
@@ -268,6 +269,13 @@ def execute(*args, **kw):
                         log.error("Could not find envelope sender attendee: %r" % (e))
                         continue
 
+                    # compare sequence number to avoid outdated replies
+                    if not itip_event['sequence'] == event.get_sequence():
+                        log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Ignoring.") % (
+                            itip_event['sequence'], event.get_sequence()
+                        ))
+                        continue
+
                     # forward owner response comment
                     comment = itip_event['xml'].get_comment()
                     if comment:
@@ -276,10 +284,12 @@ def execute(*args, **kw):
                     itip_event_ = dict(xml=event, uid=event.get_uid())
 
                     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)
-                        # TODO: set partstat=DECLINED and status=CANCELLED instead of deleting?
+                        # TODO: set status=CANCELLED instead of deleting?
+                        # event.set_status(kolabformat.StatusCancelled)
                         delete_resource_event(reference_uid, receiving_resource)
                     else:
                         log.info("Invalid response (%r) recieved from resource owner for event %r" % (
@@ -288,6 +298,9 @@ def execute(*args, **kw):
                 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
 
@@ -615,16 +628,13 @@ def accept_reservation_request(itip_event, resource, delegator=None, confirmed=F
 
     partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
 
+    itip_event['xml'].set_transparency(False);
     itip_event['xml'].set_attendee_participant_status(
         itip_event['xml'].get_attendee_by_email(resource['mail']),
         partstat
     )
 
-    # remove old copy of the reservation
-    if confirmed:
-        delete_resource_event(itip_event['uid'], resource)
-
-    saved = save_resource_event(itip_event, resource)
+    saved = save_resource_event(itip_event, resource, replace=confirmed)
 
     log.debug(
         _("Adding event to %r: %r") % (resource['kolabtargetfolder'], saved),
@@ -658,14 +668,20 @@ def decline_reservation_request(itip_event, resource):
         send_owner_notification(resource, owner, itip_event, True)
 
 
-def save_resource_event(itip_event, resource):
+def save_resource_event(itip_event, resource, replace=False):
     """
         Append the given event object to the resource's calendar
     """
     try:
         # Administrator login name comes from configuration.
         targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
-        imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+
+        # remove old copy of the reservation (also sets ACLs)
+        if replace:
+            delete_resource_event(itip_event['uid'], resource)
+        else:
+            imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+
         result = imap.imap.m.append(
             targetfolder,
             None,
@@ -788,7 +804,7 @@ def resource_records_from_itip_events(itip_events, recipient_email=None):
     log.debug(_("Raw set of resources: %r") % (resources_raw), level=9)
 
     # consider organizer (in REPLY messages), too
-    organizers_raw = [re.sub('\+[A-Za-z0-9%/_-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
+    organizers_raw = [re.sub('\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
 
     log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8)
 
@@ -1159,7 +1175,7 @@ def send_owner_confirmation(resource, owner, itip_event):
     # generate new UID and set the resource as organizer
     (mail, domain) = resource['mail'].split('@')
     event.set_uid(str(uuid.uuid4()))
-    event.set_organizer(mail + '+' + urllib.quote(uid) + '@' + domain, resource['cn'])
+    event.set_organizer(mail + '+' + base64.b64encode(uid, '-/') + '@' + domain, resource['cn'])
     itip_event['uid'] = event.get_uid()
 
     # add resource owner as (the sole) attendee


commit 78b688519c8b73d66ab1f4fba74ab39acdf9552f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 5 00:22:22 2014 -0400

    Respect transparency property for conflict detection; fix tests for translated iTip message contents

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index c30421a..ddcb392 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -144,7 +144,9 @@ def check_event_conflict(kolab_event, itip_event):
     if kolab_event.uid == itip_event['uid']:
         return conflict
 
-    # TODO: don't consider conflict if event has TRANSP:TRANSPARENT
+    # don't consider conflict if event has TRANSP:TRANSPARENT
+    if kolab_event.get_transparency():
+        return conflict
 
     _es = to_dt(kolab_event.get_start())
     _ee = to_dt(kolab_event.get_ical_dtend())  # use iCal style end date: next day for all-day events
diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py
index a120fd2..a08d05f 100644
--- a/tests/unit/test-011-itip.py
+++ b/tests/unit/test-011-itip.py
@@ -5,6 +5,8 @@ import kolabformat
 
 from pykolab import itip
 from pykolab.xml import Event
+from pykolab.xml import participant_status_label
+from pykolab.translate import _
 
 from icalendar import Calendar
 from email import message
@@ -363,6 +365,9 @@ class TestITip(unittest.TestCase):
 
         self.assertTrue(itip.check_event_conflict(allday, itip_event), "Conflicting allday event")
 
+        allday.set_transparency(True)
+        self.assertFalse(itip.check_event_conflict(allday, itip_event), "No conflict if event is set to transparent")
+
         event2 = Event()
         event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central")))
         event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central")))
@@ -398,9 +403,10 @@ class TestITip(unittest.TestCase):
         self.assertEqual(self.smtplog[0][0], 'resource-collection-car at example.org', "From attendee")
         self.assertEqual(self.smtplog[0][1], 'john.doe at example.org', "To organizer")
 
+        _accepted = participant_status_label('ACCEPTED')
         message = message_from_string(self.smtplog[0][2])
-        self.assertEqual(message.get('Subject'), 'Invitation for test was ACCEPTED')
+        self.assertEqual(message.get('Subject'), _("Invitation for %(summary)s was %(status)s") % { 'summary':'test', 'status':_accepted })
 
         text = str(message.get_payload(0));
         self.assertIn('SUMMARY=test', text)
-        self.assertIn('STATUS=ACCEPTED', text)
+        self.assertIn('STATUS=' + _accepted, text)


commit e636a436365e2025e50ed7eb16105f134412bd62
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 5 00:21:06 2014 -0400

    Remove duplicate set_status() method; get translated event status value on request

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index a9db73e..72cbfeb 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -550,11 +550,12 @@ class Event(object):
     def get_start(self):
         return xmlutils.from_cdatetime(self.event.start(), True)
 
-    def get_status(self):
+    def get_status(self, translated=False):
         status = self.event.status()
-        for key in self.status_map.keys():
-            if self.status_map[key] == status:
-                return key
+        if translated:
+            return self._translate_value(status, self.status_map) if status else None
+
+        return status
 
     def get_summary(self):
         return self.event.summary()
@@ -592,14 +593,6 @@ class Event(object):
 
         self.event.setAttendees(self._attendees)
 
-    def set_status(self, status):
-        if status in self.status_map.keys():
-            self.event.setStatus(self.status_map[status])
-        elif status in self.status_map.values():
-            self.event.setStatus(status)
-        else:
-            raise ValueError, _("Invalid status %r") % (status)
-
     def set_classification(self, classification):
         if classification in self.classification_map.keys():
             self.event.setClassification(self.classification_map[classification])


commit 91887a0e6cecf53273fb6de85608bea0b0b5581a
Merge: a71a8d2 84fd219
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 21:59:32 2014 -0400

    Merge branch 'master' of ssh://git.kolab.org/git/pykolab



commit a71a8d2729c7fb5a9fc31170cb762cb100aed7ba
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 21:59:24 2014 -0400

    Inherit kolabinvitationpolicy attributes from resource collection; forward comments from owner confirmation replies

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 7921280..4a30622 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -172,7 +172,7 @@ class Attendee(kolabformat.Attendee):
     def get_displayname(self):
         name = self.contactreference.get_name()
         email = self.contactreference.get_email()
-        return "%s <%s>" % (name, email) if name is not None else email
+        return "%s <%s>" % (name, email) if not name == "" else email
 
     def get_participant_status(self, translated=False):
         partstat = self.partStat()
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 096fba8..c21f12c 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -34,6 +34,7 @@ SUMMARY:test
 DESCRIPTION:test
 ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
 ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:%s
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Somebody Else:mailto:somebody at else.com
 TRANSP:OPAQUE
 END:VEVENT
 END:VCALENDAR
@@ -221,9 +222,11 @@ class TestResourceInvitation(unittest.TestCase):
 
         self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
         self.room2 = funcs.resource_add("confroom", "Conference Room B-222")
-        self.room3 = funcs.resource_add("confroom", "CEOs Office 303", owner=self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL')
         self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
 
+        self.room3 = funcs.resource_add("confroom", "CEOs Office 303")
+        self.viprooms = funcs.resource_add("collection", "VIP Rooms", [ self.room3['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL')
+
         time.sleep(1)
         from tests.functional.synchronize import synchronize_once
         synchronize_once()
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 2f93c6f..f7c9441 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -268,6 +268,11 @@ def execute(*args, **kw):
                         log.error("Could not find envelope sender attendee: %r" % (e))
                         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())
 
                     if owner_reply == kolabformat.PartAccepted:
@@ -598,11 +603,15 @@ def accept_reservation_request(itip_event, resource, delegator=None, confirmed=F
     owner = get_resource_owner(resource)
     confirmation_required = False
 
-    if not confirmed and resource.has_key('kolabinvitationpolicy'):
-        for policy in resource['kolabinvitationpolicy']:
-            if policy & ACT_MANUAL and owner['mail']:
-                confirmation_required = True
-                break
+    if not confirmed:
+        invitationpolicy = get_resource_invitationpolicy(resource)
+        log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=9)
+
+        if invitationpolicy is not None:
+            for policy in invitationpolicy:
+                if policy & ACT_MANUAL and owner['mail']:
+                    confirmation_required = True
+                    break
 
     partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
 
@@ -963,6 +972,37 @@ def get_resource_owner(resource):
     return None
 
 
+def get_resource_invitationpolicy(resource):
+    """
+        Get this resource's kolabinvitationpolicy configuration
+    """
+    global auth
+
+    if not resource.has_key('kolabinvitationpolicy') or resource['kolabinvitationpolicy'] is None:
+        if not auth:
+            auth = Auth()
+            auth.connect()
+
+        # get kolabinvitationpolicy attribute from collection
+        collections = auth.search_entry_by_attribute('uniquemember', resource['dn'])
+        if not isinstance(collections, list):
+            collections = [ (collections['dn'],collections) ]
+
+        log.debug("Check collections %r for kolabinvitationpolicy attributes" % (collections), level=9)
+
+        for dn,collection in collections:
+            # ldap.search_entry_by_attribute() doesn't return the attributes lower-cased
+            if collection.has_key('kolabInvitationPolicy'):
+                collection['kolabinvitationpolicy'] = collection['kolabInvitationPolicy']
+
+            if collection.has_key('kolabinvitationpolicy'):
+                parse_kolabinvitationpolicy(collection)
+                resource['kolabinvitationpolicy'] = collection['kolabinvitationpolicy']
+                break
+
+    return resource['kolabinvitationpolicy'] if resource.has_key('kolabinvitationpolicy') else None
+
+
 def send_response(from_address, itip_events, owner=None):
     """
         Send the given iCal events as a valid iTip response to the organizer.
@@ -1033,8 +1073,10 @@ def send_owner_notification(resource, owner, itip_event, success=True):
     notify = False
     status = itip_event['xml'].get_attendee_by_email(resource['mail']).get_participant_status(True)
 
-    if resource.has_key('kolabinvitationpolicy'):
-        for policy in resource['kolabinvitationpolicy']:
+    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
@@ -1109,9 +1151,10 @@ def send_owner_confirmation(resource, owner, itip_event):
         receive the reply from the owner.
     """
 
-    event = itip_event['xml']
     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]
 
     # generate new UID and set the resource as organizer
     (mail, domain) = resource['mail'].split('@')
@@ -1119,11 +1162,12 @@ def send_owner_confirmation(resource, owner, itip_event):
     event.set_organizer(mail + '+' + urllib.quote(uid) + '@' + domain, resource['cn'])
     itip_event['uid'] = event.get_uid()
 
-    # add resource owner as attendee
+    # 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-Wallace-MessageType', 'CONFIRMATION')
+    event.add_custom_property('X-Kolab-InvitationType', 'CONFIRMATION')
 
     log.debug(
         _("Clone invitation for owner confirmation: %r from %r") % (
@@ -1140,6 +1184,7 @@ def send_owner_confirmation(resource, owner, itip_event):
 
         Subject: %(summary)s.
         Date: %(date)s
+        Participants: %(attendees)s
 
         *** This is an automated message, please don't reply by email. ***
     """)% {
@@ -1147,7 +1192,8 @@ def send_owner_confirmation(resource, owner, itip_event):
         'orgname': organizer.name(),
         'orgemail': organizer.email(),
         'summary': event.get_summary(),
-        'date': event.get_date_text()
+        'date': event.get_date_text(),
+        'attendees': ",\n+ ".join(event_attendees)
     }
 
     pykolab.itip.send_request(owner['mail'], itip_event, message_text,


commit acbede72e43a4f38a1cb31e920a06dcdd13be2d2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 20:26:48 2014 -0400

    Treat the comment property as list for iCal export

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 076eb39..a9db73e 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -521,6 +521,12 @@ class Event(object):
     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_location(self):
         return self.event.location()
 


commit 84fd219265a9c589d5031e9476677c0a3f59bab4
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon Aug 4 21:41:09 2014 +0200

    Rename folders correctly

diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py
index f09e429..6f561ce 100644
--- a/pykolab/imap/cyrus.py
+++ b/pykolab/imap/cyrus.py
@@ -123,6 +123,7 @@ class Cyrus(cyruslib.CYRUS):
 
         imap = IMAP()
         imap.connect(uri=uri)
+
         if not self.SEP == self.separator:
             self.separator = self.SEP
 
@@ -236,8 +237,12 @@ class Cyrus(cyruslib.CYRUS):
         server = self.find_mailfolder_server(from_mailfolder)
         self.connect(self.uri.replace(self.server,server))
 
-        log.debug(_("Moving INBOX folder %s to %s") % (from_mailfolder,to_mailfolder), level=8)
-        self.m.rename(from_mailfolder, to_mailfolder, partition)
+        if not partition == None:
+            log.debug(_("Moving INBOX folder %s to %s on partition %s") % (from_mailfolder,to_mailfolder, partition), level=8)
+        else:
+            log.debug(_("Moving INBOX folder %s to %s") % (from_mailfolder,to_mailfolder), level=8)
+
+        self.m.rename(self.folder_utf7(from_mailfolder), self.folder_utf7(to_mailfolder), '"%s"' % (partition))
 
     def _getannotation(self, *args, **kw):
         return self.getannotation(*args, **kw)


commit 49acbe0b394d91c7fe7f6bd92513efcbb544978c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon Aug 4 21:40:05 2014 +0200

    Only recognize the folder as already existing, if no partition is specified

diff --git a/pykolab/cli/cmd_rename_mailbox.py b/pykolab/cli/cmd_rename_mailbox.py
index 87d41f3..4efed0d 100644
--- a/pykolab/cli/cmd_rename_mailbox.py
+++ b/pykolab/cli/cmd_rename_mailbox.py
@@ -66,7 +66,7 @@ def execute(*args, **kw):
         print >> sys.stderr, _("Source folder %r does not exist") % (source_folder)
         sys.exit(1)
 
-    if imap.has_folder(target_folder):
+    if imap.has_folder(target_folder) and partition == None:
         print >> sys.stderr, _("Target folder %r already exists") % (target_folder)
         sys.exit(1)
 


commit a15fe609702bffb00e6fb47e5129acce2798a07b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon Aug 4 21:39:35 2014 +0200

    Be compatible with what the Kolab Web Administration Panel is writing out

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 752b4fc..f15d2c8 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1816,9 +1816,26 @@ class LDAP(pykolab.base.Base):
         if entry.has_key('kolabfolderaclentry') and \
                 not entry['kolabfolderaclentry'] == None:
 
+            if isinstance(entry['kolabfolderaclentry'], basestring):
+                entry['kolabfolderaclentry'] = [ entry['kolabfolderaclentry'] ]
+
+            import copy
+            _acls = copy.deepcopy(entry['kolabfolderaclentry'])
+            entry['kolabfolderaclentry'] = []
+
+            for _entry in _acls:
+                if _entry[0] == "(":
+                    entry['kolabfolderaclentry'].append(_entry)
+                    continue
+
+                s,r = [x.strip() for x in _entry.split(',')]
+
+                entry['kolabfolderaclentry'].append("('%s', '%s', '%s')" % (folder_path, s, r))
+
             self.imap._set_kolab_mailfolder_acls(
                     entry['kolabfolderaclentry']
                 )
+
         elif entry['kolabfolderaclentry'] in [None,[]]:
             for ace in self.imap.list_acls(folder_path):
                 aci_subject = ace.split()[0]


commit 542ec1d797130b674ed46a9d7a7e94c056f34266
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon Aug 4 21:38:55 2014 +0200

    Add iTip module

diff --git a/pykolab/Makefile.am b/pykolab/Makefile.am
index a23aa6e..f2c4cf4 100644
--- a/pykolab/Makefile.am
+++ b/pykolab/Makefile.am
@@ -42,6 +42,10 @@ pykolab_imap_PYTHON = \
 	imap/__init__.py \
 	imap/cyrus.py
 
+pykolab_itipdir = $(pythondir)/$(PACKAGE)/itip
+pykolab_itip_PYTHON = \
+	itip/__init__.py
+
 pykolab_pluginsdir = $(pythondir)/$(PACKAGE)/plugins
 pykolab_plugins_PYTHON = \
 	plugins/__init__.py


commit d9f5e3568f9d9298ea44194c8c10cf547652a5e1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 13:44:56 2014 -0400

    First attempt for resource owner confirmation workflow as described in #3168

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 40cf007..c30421a 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -241,7 +241,7 @@ def send_reply(from_address, itip_events, response_text, subject=None):
         smtp.quit()
 
 
-def send_request(to_address, itip_events, request_text, subject=None):
+def send_request(to_address, itip_events, request_text, subject=None, direct=False):
     """
         Send an iTip REQUEST message from the given iCal events
     """
@@ -270,7 +270,8 @@ def send_request(to_address, itip_events, request_text, subject=None):
             log.error(_("Failed to compose iTip request message: %r") % (e))
             return
 
-        smtp = smtplib.SMTP("localhost", 10026)  # requests go through wallace
+        port = 10027 if direct else 10026
+        smtp = smtplib.SMTP("localhost", port)
 
         if conf.debuglevel > 8:
             smtp.set_debuglevel(True)
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 534134f..076eb39 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -644,7 +644,7 @@ class Event(object):
             raise ValueError, _("Invalid custom property name %r") % (name)
 
         props = self.event.customProperties()
-        props.append(kolabformat.CustomProperty(name, value))
+        props.append(kolabformat.CustomProperty(name.upper(), value))
         self.event.setCustomProperties(props)
 
     def set_from_ical(self, attr, value):
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 60b6587..096fba8 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -11,6 +11,7 @@ from wallace import module_resources
 from pykolab.translate import _
 from pykolab.xml import event_from_message
 from pykolab.xml import participant_status_label
+from pykolab.itip import events_from_message
 from email import message_from_string
 from twisted.trial import unittest
 
@@ -220,6 +221,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
         self.room2 = funcs.resource_add("confroom", "Conference Room B-222")
+        self.room3 = funcs.resource_add("confroom", "CEOs Office 303", owner=self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL')
         self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
 
         time.sleep(1)
@@ -232,6 +234,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         smtp = smtplib.SMTP('localhost', 10026)
         smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload))
+        smtp.quit()
 
     def send_itip_invitation(self, resource_email, start=None, allday=False, template=None):
         if start is None:
@@ -339,6 +342,8 @@ class TestResourceInvitation(unittest.TestCase):
 
             time.sleep(1)
 
+        imap.disconnect()
+
         return found
 
     def purge_mailbox(self, mailbox):
@@ -621,3 +626,82 @@ class TestResourceInvitation(unittest.TestCase):
         notify = self.check_message_received(_('Booking for %s has been %s') % (delegatee['cn'], participant_status_label('ACCEPTED')), delegatee['mail'], self.jane['mailbox'])
         self.assertIsInstance(notify, email.message.Message)
         self.assertIn(self.john['mail'], notification_text)
+
+
+    def test_013_owner_confirmation_accept(self):
+        self.purge_mailbox(self.john['mailbox'])
+        self.purge_mailbox(self.jane['mailbox'])
+
+        uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,9,12, 14,0,0))
+
+        # requester (john) gets a TENTATIVE confirmation
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test")
+        self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE')
+
+        # check confirmation message sent to resource owner (jane)
+        notify = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notify, email.message.Message)
+
+        itip_event = events_from_message(notify)[0]
+
+        # resource owner confirms reservation request
+        itip_reply = itip_event['xml'].to_message_itip(self.jane['mail'],
+            method="REPLY",
+            participant_status='ACCEPTED',
+            message_text="Request accepted",
+            subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED'))
+        )
+
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(self.jane['mail'], str(itip_event['organizer']), str(itip_reply))
+        smtp.quit()
+
+        # requester (john) now gets the ACCEPTED response
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
+
+
+    def test_014_owner_confirmation_decline(self):
+        self.purge_mailbox(self.john['mailbox'])
+        self.purge_mailbox(self.jane['mailbox'])
+
+        uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,9,14, 9,0,0))
+
+        # requester (john) gets a TENTATIVE confirmation
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        # check confirmation message sent to resource owner (jane)
+        notify = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notify, email.message.Message)
+
+        itip_event = events_from_message(notify)[0]
+
+        # resource owner declines reservation request
+        itip_reply = itip_event['xml'].to_message_itip(self.jane['mail'],
+            method="REPLY",
+            participant_status='DECLINED',
+            message_text="Request declined",
+            subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED'))
+        )
+
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(self.jane['mail'], str(itip_event['organizer']), str(itip_reply))
+        smtp.quit()
+
+        # requester (john) now gets the DECLINED response
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.room3['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        # tentative reservation was removed from resource calendar
+        event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+        self.assertEqual(event, None)
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 45a817c..1f54419 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -417,7 +417,7 @@ END:VEVENT
         self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))
         self.event.set_end(datetime.datetime(2014, 05, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London")))
         self.event.set_sequence(3)
-        self.event.add_custom_property('X-CUSTOM', 'check')
+        self.event.add_custom_property('X-Custom', 'check')
 
         ical = icalendar.Calendar.from_ical(self.event.as_string_itip())
         event = ical.walk('VEVENT')[0]
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index b31a8d0..2f93c6f 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -26,6 +26,8 @@ import tempfile
 import time
 from urlparse import urlparse
 import urllib
+import uuid
+import re
 
 from email import message_from_string
 from email.parser import Parser
@@ -159,15 +161,17 @@ def execute(*args, **kw):
     message = Parser().parse(open(filepath, 'r'))
 
     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 = True
+    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', 'CANCEL'])
+        itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL'])
     except Exception, e:
         log.error(_("Failed to parse iTip events from message: %r" % (e)))
         itip_events = []
@@ -199,6 +203,12 @@ def execute(*args, **kw):
         auth.connect()
 
         for recipient in recipients:
+            # extract reference UID from recipients like resource+UID at domain.org
+            if re.match('.+\+[A-Za-z0-9%/_-]+@', recipient):
+                (prefix, host) = recipient.split('@')
+                (local, reference_uid) = prefix.split('+')
+                recipient = local + '@' + host
+
             if not len(resource_record_from_email_address(recipient)) == 0:
                 resource_recipient = recipient
                 any_resources = True
@@ -226,6 +236,7 @@ def execute(*args, **kw):
     # 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
 
@@ -242,6 +253,41 @@ def execute(*args, **kw):
     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 = find_existing_event(reference_uid, receiving_resource)
+                if event:
+                    try:
+                        sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email)
+                        owner_reply = sender_attendee.get_participant_status()
+                        log.debug(_("Sender Attendee: %r => %r") % (sender_attendee, owner_reply), level=9)
+                    except Exception, e:
+                        log.error("Could not find envelope sender attendee: %r" % (e))
+                        continue
+
+                    itip_event_ = dict(xml=event, uid=event.get_uid())
+
+                    if owner_reply == kolabformat.PartAccepted:
+                        accept_reservation_request(itip_event_, receiving_resource, confirmed=True)
+                    elif owner_reply == kolabformat.PartDeclined:
+                        decline_reservation_request(itip_event_, receiving_resource)
+                        # TODO: set partstat=DECLINED and status=CANCELLED instead of deleting?
+                        delete_resource_event(reference_uid, receiving_resource)
+                    else:
+                        log.info("Invalid response (%r) recieved 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))
+
+            # exit for-loop
+            break
+
+        # else:
+
         try:
             receiving_attendee = itip_event['xml'].get_attendee_by_email(receiving_resource['mail'])
             log.debug(_("Receiving Resource: %r; %r") % (receiving_resource, receiving_attendee), level=9)
@@ -510,18 +556,65 @@ def read_resource_calendar(resource_rec, itip_events):
     return num_messages
 
 
-def accept_reservation_request(itip_event, resource, delegator=None):
+def find_existing_event(uid, resource_rec):
+    """
+        Search the resources's calendar folder for the given event (by UID)
+    """
+    global imap
+
+    event = None
+    mailbox = resource_rec['kolabtargetfolder']
+
+    log.debug(_("Searching %r for event %r") % (mailbox, uid), level=9)
+
+    try:
+        imap.imap.m.select(imap.folder_quote(mailbox))
+        typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
+    except Exception, e:
+        log.error(_("Failed to access resource calendar:: %r") % (e))
+        return event
+
+    for num in reversed(data[0].split()):
+        typ, data = imap.imap.m.fetch(num, '(RFC822)')
+
+        try:
+            event = event_from_message(message_from_string(data[0][1]))
+        except Exception, e:
+            log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e))
+            continue
+
+        if event and event.uid == uid:
+            return event
+
+    return event
+
+
+def accept_reservation_request(itip_event, resource, delegator=None, confirmed=False):
     """
         Accepts the given iTip event by booking it into the resource's
         calendar. Then set the attendee status of the given resource to
         ACCEPTED and sends an iTip reply message to the organizer.
     """
+    owner = get_resource_owner(resource)
+    confirmation_required = False
+
+    if not confirmed and resource.has_key('kolabinvitationpolicy'):
+        for policy in resource['kolabinvitationpolicy']:
+            if policy & ACT_MANUAL and owner['mail']:
+                confirmation_required = True
+                break
+
+    partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
 
     itip_event['xml'].set_attendee_participant_status(
         itip_event['xml'].get_attendee_by_email(resource['mail']),
-        "ACCEPTED"
+        partstat
     )
 
+    # remove old copy of the reservation
+    if confirmed:
+        delete_resource_event(itip_event['uid'], resource)
+
     saved = save_resource_event(itip_event, resource)
 
     log.debug(
@@ -529,12 +622,12 @@ def accept_reservation_request(itip_event, resource, delegator=None):
         level=8
     )
 
-    owner = get_resource_owner(resource)
-
     if saved:
         send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner)
 
-    if owner:
+    if owner and confirmation_required:
+        send_owner_confirmation(resource, owner, itip_event)
+    elif owner:
         send_owner_notification(resource, owner, itip_event, saved)
 
 
@@ -685,6 +778,12 @@ def resource_records_from_itip_events(itip_events, recipient_email=None):
 
     log.debug(_("Raw set of resources: %r") % (resources_raw), level=9)
 
+    # consider organizer (in REPLY messages), too
+    organizers_raw = [re.sub('\+[A-Za-z0-9%/_-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
+
+    log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8)
+
+
     # TODO: We expect the format of an attendee line to literally be:
     #
     #   ATTENDEE:RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers at kolabsys.com
@@ -693,7 +792,7 @@ def resource_records_from_itip_events(itip_events, recipient_email=None):
     #
     #   RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers at kolabsys.com
     #
-    attendees = [x.split(':')[-1] for x in attendees_raw]
+    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
@@ -1000,3 +1099,59 @@ def owner_notification_text(resource, owner, event, success):
         'orgname': organizer.name(),
         'orgemail': organizer.email()
     }
+
+
+def send_owner_confirmation(resource, owner, itip_event):
+    """
+        Send a reservation request to the resource owner for manual confirmation (ACCEPT or DECLINE)
+
+        This clones the given invtation with a new UID and setting the resource as organizer in order to
+        receive the reply from the owner.
+    """
+
+    event = itip_event['xml']
+    uid = itip_event['uid']
+    organizer = event.get_organizer()
+
+    # generate new UID and set the resource as organizer
+    (mail, domain) = resource['mail'].split('@')
+    event.set_uid(str(uuid.uuid4()))
+    event.set_organizer(mail + '+' + urllib.quote(uid) + '@' + domain, resource['cn'])
+    itip_event['uid'] = event.get_uid()
+
+    # add resource owner as attendee
+    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-Wallace-MessageType', 'CONFIRMATION')
+
+    log.debug(
+        _("Clone invitation for owner confirmation: %r from %r") % (
+            itip_event['uid'], event.get_organizer().email()
+        ),
+        level=8
+    )
+
+    message_text = _("""
+        A reservation request for %(resource)s requires your approval!
+        Please either accept or decline this inivitation without saving it to your calendar.
+
+        The reservation request was sent from %(orgname)s <%(orgemail)s>.
+
+        Subject: %(summary)s.
+        Date: %(date)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()
+    }
+
+    pykolab.itip.send_request(owner['mail'], itip_event, message_text,
+        subject=_('Booking request for %s requires confirmation') % (resource['cn']),
+        direct=True)
+
+


commit 5a171ddd85f7f8e57685a469d3de3fc3bb6a99ab
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 12:49:51 2014 -0400

    Allow to set custom event properties and add them in iCal export

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 213e43e..534134f 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -197,6 +197,10 @@ class Event(object):
                 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)
+
         cal.add_component(event)
 
         if hasattr(cal, 'to_ical'):
@@ -635,6 +639,14 @@ class Event(object):
         for _datetime in _datetimes:
             self.add_exception_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, value))
+        self.event.setCustomProperties(props)
+
     def set_from_ical(self, attr, value):
         ical_setter = 'set_ical_' + attr
         default_setter = 'set_' + attr
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index f069be3..45a817c 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -417,6 +417,7 @@ END:VEVENT
         self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))
         self.event.set_end(datetime.datetime(2014, 05, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London")))
         self.event.set_sequence(3)
+        self.event.add_custom_property('X-CUSTOM', 'check')
 
         ical = icalendar.Calendar.from_ical(self.event.as_string_itip())
         event = ical.walk('VEVENT')[0]
@@ -424,6 +425,7 @@ END:VEVENT
         self.assertEqual(event['uid'], self.event.get_uid())
         self.assertEqual(event['summary'], "test")
         self.assertEqual(event['sequence'], 3)
+        self.assertEqual(event['X-CUSTOM'], "check")
         self.assertIsInstance(event['dtstamp'].dt, datetime.datetime)
 
     def test_020_calendaring_recurrence(self):


commit 0813c350ed72c595bdee53e9c78490d8eccf7e98
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 12:32:24 2014 -0400

    Add function to send iTip REQUESTs

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 816ee1d..40cf007 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -113,7 +113,7 @@ def objects_from_message(message, objname, methods=None):
                         # TODO: distinguish event and todo here
                         itip['xml'] = event_from_ical(c.to_ical())
                     except Exception, e:
-                        log.error("event_from_ical() exception: %r" % (e))
+                        log.error("event_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
                         continue
 
                     itip_objects.append(itip)
@@ -198,10 +198,10 @@ def send_reply(from_address, itip_events, response_text, subject=None):
     """
         Send the given iCal events as a valid iTip REPLY to the organizer.
     """
-
     import smtplib
 
     conf = pykolab.getConf()
+    smtp = None
 
     if isinstance(itip_events, dict):
         itip_events = [ itip_events ]
@@ -237,4 +237,48 @@ def send_reply(from_address, itip_events, response_text, subject=None):
         except Exception, e:
             log.error(_("SMTP sendmail error: %r") % (e))
 
-    smtp.quit()
+    if smtp:
+        smtp.quit()
+
+
+def send_request(to_address, itip_events, request_text, subject=None):
+    """
+        Send an iTip REQUEST message from the given iCal events
+    """
+    import smtplib
+
+    conf = pykolab.getConf()
+    smtp = None
+
+    if isinstance(itip_events, dict):
+        itip_events = [ itip_events ]
+
+    for itip_event in itip_events:
+        event_summary = itip_event['xml'].get_summary()
+        message_text = request_text % { 'summary':event_summary }
+
+        if subject is not None:
+            subject = subject % { 'summary':event_summary }
+
+        try:
+            message = itip_event['xml'].to_message_itip(None,
+                method="REQUEST",
+                message_text=message_text,
+                subject=subject
+            )
+        except Exception, e:
+            log.error(_("Failed to compose iTip request message: %r") % (e))
+            return
+
+        smtp = smtplib.SMTP("localhost", 10026)  # requests go through wallace
+
+        if conf.debuglevel > 8:
+            smtp.set_debuglevel(True)
+
+        try:
+            smtp.sendmail(message['From'], to_address, message.as_string())
+        except Exception, e:
+            log.error(_("SMTP sendmail error: %r") % (e))
+
+    if smtp:
+        smtp.quit()


commit 70139312f2b09837c5e9c95e81888d2796a4d7e2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 08:12:59 2014 -0400

    User DN can be False if not found

diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index de29450..9e0fa37 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -227,7 +227,7 @@ def execute(*args, **kw):
 
         for recipient in recipients:
             recipient_user_dn = user_dn_from_email_address(recipient)
-            if recipient_user_dn is not None:
+            if recipient_user_dn:
                 recipient_email = recipient
                 break
 


commit 8980bcea3d2a1c7d3349ee99ad9c8978cbcc75a3
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Aug 4 08:12:24 2014 -0400

    Return None if event status is not set

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 75b00ac..213e43e 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -512,7 +512,7 @@ class Event(object):
         if status in self.status_map.keys():
             return status
 
-        return self._translate_value(status, self.status_map)
+        return self._translate_value(status, self.status_map) if status else None
 
     def get_ical_sequence(self):
         return str(self.event.sequence()) if self.event.sequence() else None


commit 13661b09f90b461f5d8e0fd6f73d7639b09fbe9e
Merge: eb2c8e7 5aa6b40
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Aug 1 22:11:44 2014 +0200

    Merge branch 'master' of ssh://git.kolabsys.com/git/pykolab



commit eb2c8e73da5af5fd02244963055fcb7e26993adb
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Aug 1 22:09:46 2014 +0200

    Update the list of short acl rights (read, write, etc.)

diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index 0b2b8cd..f52dc9f 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -320,11 +320,15 @@ class IMAP(object):
             Set an ACL entry on a folder.
         """
         short_rights = {
-                'all': 'lrswipkxtecda',
+                'all': 'lrsedntxakcpiw',
+                'append': 'wip',
+                'full': 'lrswipkxtecdn',
+                'read': 'lrs',
                 'read-only': 'lrs',
-                'read-write': 'lrswited',
+                'read-write': 'lrswitedn',
+                'post': 'p',
                 'semi-full': 'lrswit',
-                'full': 'lrswipkxtecd'
+                'write': 'lrswite',
             }
 
         if short_rights.has_key(acl):


commit 55de288c17e7b7861b7473a1c3e5c7e5bcda7248
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Aug 1 22:09:11 2014 +0200

    Make sure we apply access control, including when no access control is specified (remove the default acl)

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 7bfccd7..752b4fc 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1215,46 +1215,45 @@ class LDAP(pykolab.base.Base):
                 folder_path = entry['cn']
 
         folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute')
+        if folderacl_entry_attribute == None:
+            folderacl_entry_attribute = 'acl'
 
-        if not folderacl_entry_attribute == None:
-            if not entry.has_key(folderacl_entry_attribute):
-                entry[folderacl_entry_attribute] = self.get_entry_attribute(
-                        entry['id'],
-                        folderacl_entry_attribute
-                    )
-
-            if not entry[folderacl_entry_attribute] == None:
-                # Parse it before assigning it
-                entry['kolabmailfolderaclentry'] = []
-                if not isinstance(entry[folderacl_entry_attribute], list):
-                    entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ]
+        if not entry.has_key(folderacl_entry_attribute):
+            entry[folderacl_entry_attribute] = self.get_entry_attribute(
+                    entry['id'],
+                    folderacl_entry_attribute
+                )
 
-                for acl_entry in entry[folderacl_entry_attribute]:
-                    acl_access = acl_entry.split()[-1]
-                    aci_subject = ' '.join(acl_entry.split()[:-1])
+        if not entry[folderacl_entry_attribute] == None:
+            # Parse it before assigning it
+            entry['kolabfolderaclentry'] = []
+            if not isinstance(entry[folderacl_entry_attribute], list):
+                entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ]
 
-                    log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
+            for acl_entry in entry[folderacl_entry_attribute]:
+                acl_access = acl_entry.split()[-1]
+                aci_subject = ' '.join(acl_entry.split()[:-1])
 
-                    access_lookup_dict = {
-                            'read': 'lrs',
-                            'post': 'p',
-                            'append': 'wip',
-                            'write': 'lrswite',
-                            'all': 'lrsedntxakcpiw'
-                        }
+                log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 
-                    if access_lookup_dict.has_key(acl_access):
-                        acl_access = access_lookup_dict[acl_access]
+                access_lookup_dict = {
+                        'all': 'lrsedntxakcpiw',
+                        'append': 'wip',
+                        'full': 'lrswipkxtecdn',
+                        'read': 'lrs',
+                        'read-only': 'lrs',
+                        'read-write': 'lrswitedn',
+                        'post': 'p',
+                        'semi-full': 'lrswit',
+                        'write': 'lrswite',
+                    }
 
-                    log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
+                if access_lookup_dict.has_key(acl_access):
+                    acl_access = access_lookup_dict[acl_access]
 
-                    entry['kolabmailfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
+                log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 
-        if not entry.has_key('kolabmailfolderaclentry'):
-            entry['kolabmailfolderaclentry'] = self.get_entry_attribute(
-                    entry['id'],
-                    'kolabmailfolderaclentry'
-                )
+                entry['kolabfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
 
         if not self.imap.shared_folder_exists(folder_path):
             self.imap.shared_folder_create(folder_path, server)
@@ -1267,12 +1266,14 @@ class LDAP(pykolab.base.Base):
                     entry['kolabfoldertype']
                 )
 
-        if entry.has_key('kolabmailfolderaclentry') and \
-                not entry['kolabmailfolderaclentry'] == None:
+        if entry.has_key('kolabfolderaclentry') and \
+                not entry['kolabfolderaclentry'] == None:
 
             self.imap._set_kolab_mailfolder_acls(
-                    entry['kolabmailfolderaclentry']
+                    entry['kolabfolderaclentry']
                 )
+        else:
+            self.imap.set_acl(folder_path, 'anyone', '')
 
         if entry.has_key(delivery_address_attribute) and \
                 not entry[delivery_address_attribute] == None:
@@ -1593,46 +1594,45 @@ class LDAP(pykolab.base.Base):
                 folder_path = entry['cn']
 
         folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute')
+        if folderacl_entry_attribute == None:
+            folderacl_entry_attribute = 'acl'
 
-        if not folderacl_entry_attribute == None:
-            if not entry.has_key(folderacl_entry_attribute):
-                entry[folderacl_entry_attribute] = self.get_entry_attribute(
-                        entry['id'],
-                        folderacl_entry_attribute
-                    )
-
-            if not entry[folderacl_entry_attribute] == None:
-                # Parse it before assigning it
-                entry['kolabmailfolderaclentry'] = []
-                if not isinstance(entry[folderacl_entry_attribute], list):
-                    entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ]
+        if not entry.has_key(folderacl_entry_attribute):
+            entry[folderacl_entry_attribute] = self.get_entry_attribute(
+                    entry['id'],
+                    folderacl_entry_attribute
+                )
 
-                for acl_entry in entry[folderacl_entry_attribute]:
-                    acl_access = acl_entry.split()[-1]
-                    aci_subject = ' '.join(acl_entry.split()[:-1])
+        if not entry[folderacl_entry_attribute] == None:
+            # Parse it before assigning it
+            entry['kolabfolderaclentry'] = []
+            if not isinstance(entry[folderacl_entry_attribute], list):
+                entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ]
 
-                    log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
+            for acl_entry in entry[folderacl_entry_attribute]:
+                acl_access = acl_entry.split()[-1]
+                aci_subject = ' '.join(acl_entry.split()[:-1])
 
-                    access_lookup_dict = {
-                            'read': 'lrs',
-                            'post': 'p',
-                            'append': 'wip',
-                            'write': 'lrswite',
-                            'all': 'lrsedntxakcpiw'
-                        }
+                log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 
-                    if access_lookup_dict.has_key(acl_access):
-                        acl_access = access_lookup_dict[acl_access]
+                access_lookup_dict = {
+                        'all': 'lrsedntxakcpiw',
+                        'append': 'wip',
+                        'full': 'lrswipkxtecdn',
+                        'read': 'lrs',
+                        'read-only': 'lrs',
+                        'read-write': 'lrswitedn',
+                        'post': 'p',
+                        'semi-full': 'lrswit',
+                        'write': 'lrswite',
+                    }
 
-                    log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
+                if access_lookup_dict.has_key(acl_access):
+                    acl_access = access_lookup_dict[acl_access]
 
-                    entry['kolabmailfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
+                log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8)
 
-        if not entry.has_key('kolabmailfolderaclentry'):
-            entry['kolabmailfolderaclentry'] = self.get_entry_attribute(
-                    entry['id'],
-                    'kolabmailfolderaclentry'
-                )
+                entry['kolabfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access))
 
         if not self.imap.shared_folder_exists(folder_path):
             self.imap.shared_folder_create(folder_path, server)
@@ -1644,20 +1644,21 @@ class LDAP(pykolab.base.Base):
                     folder_path,
                     entry['kolabfoldertype']
                 )
+        else:
+            self.imap.set_acl(folder_path, 'anyone', '')
 
-        if entry.has_key(delivery_address_attribute) and \
-                not entry[delivery_address_attribute] == None:
-            self.imap.set_acl(folder_path, 'anyone', 'p')
-
-        if entry.has_key('kolabmailfolderaclentry') and \
-                not entry['kolabmailfolderaclentry'] == None:
+        if entry.has_key('kolabfolderaclentry') and \
+                not entry['kolabfolderaclentry'] == None:
 
             self.imap._set_kolab_mailfolder_acls(
-                    entry['kolabmailfolderaclentry']
+                    entry['kolabfolderaclentry']
                 )
+        else:
+            self.imap.set_acl(folder_path, 'anyone', '')
 
-        #if server == None:
-            #self.entry_set_attribute(mailserver_attribute, server)
+        if entry.has_key(delivery_address_attribute) and \
+                not entry[delivery_address_attribute] == None:
+            self.imap.set_acl(folder_path, 'anyone', 'p')
 
     def _change_modify_user(self, entry, change):
         """
@@ -1772,11 +1773,18 @@ class LDAP(pykolab.base.Base):
                     'kolabfoldertype'
                 )
 
-        #if not entry.has_key('kolabmailfolderaclentry'):
-            #entry['kolabmailfolderaclentry'] = self.get_entry_attribute(
-                    #entry['id'],
-                    #'kolabmailfolderaclentry'
-                #)
+        folderacl_entry_attribute = conf.get('ldap', 'folderacl_entry_attribute')
+        if folderacl_entry_attribute == None:
+            folderacl_entry_attribute = 'acl'
+
+        if not entry.has_key(folderacl_entry_attribute):
+            entry['kolabfolderaclentry'] = self.get_entry_attribute(
+                    entry['id'],
+                    folderacl_entry_attribute
+                )
+        else:
+            entry['kolabfolderaclentry'] = entry[folderacl_entry_attribute]
+            del entry[folderacl_entry_attribute]
 
         if entry.has_key('kolabtargetfolder') and \
                 not entry['kolabtargetfolder'] == None:
@@ -1805,12 +1813,16 @@ class LDAP(pykolab.base.Base):
                     entry['kolabfoldertype']
                 )
 
-        if entry.has_key('kolabmailfolderaclentry') and \
-                not entry['kolabmailfolderaclentry'] == None:
+        if entry.has_key('kolabfolderaclentry') and \
+                not entry['kolabfolderaclentry'] == None:
 
             self.imap._set_kolab_mailfolder_acls(
-                    entry['kolabmailfolderaclentry']
+                    entry['kolabfolderaclentry']
                 )
+        elif entry['kolabfolderaclentry'] in [None,[]]:
+            for ace in self.imap.list_acls(folder_path):
+                aci_subject = ace.split()[0]
+                self.imap.set_acl(folder_path, aci_subject, '')
 
         delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute')
         if entry.has_key(delivery_address_attribute) and \


commit 1289a04ec39066c32fa6905f528dc06f30684443
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Aug 1 22:07:42 2014 +0200

    Update for the new domain listing

diff --git a/pykolab/cli/cmd_count_domain_mailboxes.py b/pykolab/cli/cmd_count_domain_mailboxes.py
index 8aecd2d..958bccd 100644
--- a/pykolab/cli/cmd_count_domain_mailboxes.py
+++ b/pykolab/cli/cmd_count_domain_mailboxes.py
@@ -56,10 +56,8 @@ def execute(*args, **kw):
     domains = auth.list_domains()
 
     folders = []
-    for primary,secondaries in domains:
-        print "%s: %d" % (primary,len(imap.lm("user/%%@%s" % (primary))))
-        for secondary in secondaries:
-            print "%s: %d" % (secondary,len(imap.lm("user/%%@%s" % (secondary))))
+    for domain in domains.keys():
+        print "%s: %d" % (domain,len(imap.lm("user/%%@%s" % (domain))))
 
     null_realm = len(imap.lm("user/%%"))
 


commit 8c4c9e8643dc4489a42f22e5a25b737103169b5b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Aug 1 21:49:39 2014 +0200

    Add group functions

diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index bf35f79..9549fe8 100644
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@ -303,10 +303,18 @@ def group_form_value_generate_mail(params=None):
 
     return request('POST', 'group_form_value.generate_mail', params)
 
-def group_info():
-    group = utils.ask_question("Group email address")
-    group = request('GET', 'group.info?group=%s' % (group))
-    return group
+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:


commit f1b19fd7d9eab2b110a317e50141dd74d3497090
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 17:12:01 2014 +0200

    Relay rename to the backend

diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index 6cec86f..0b2b8cd 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -652,7 +652,7 @@ class IMAP(object):
             if additional_folders[additional_folder].has_key("partition"):
                 partition = additional_folders[additional_folder]["partition"]
                 try:
-                    self.imap.rename(folder_name, folder_name, partition)
+                    self.imap._rename(folder_name, folder_name, partition)
                 except:
                     log.error(_("Could not rename %s to reside on partition %s") % (folder_name, partition))
 


commit 0bae4c3a4a95245406f4f3f2f59e5cfbe9138ead
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 15:10:24 2014 +0200

    Add a command list-users

diff --git a/pykolab/cli/cmd_list_users.py b/pykolab/cli/cmd_list_users.py
new file mode 100644
index 0000000..ff1ddef
--- /dev/null
+++ b/pykolab/cli/cmd_list_users.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com)
+#
+# Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen a kolabsys.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+import commands
+
+import pykolab
+
+from pykolab import utils
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.cli')
+conf = pykolab.getConf()
+
+def __init__():
+    commands.register('list_users', execute, description="List organizational units.")
+
+def execute(*args, **kw):
+    from pykolab import wap_client
+
+    wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw"))
+
+    users = wap_client.users_list()
+    print '\n'.join(users['list'].keys())


commit 1105186d786bd1764b3c3b2cfb09ec6c97365f38
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 15:09:44 2014 +0200

    Add a variety of useful wap_client functions to write functional tests on top of

diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index 299b847..bf35f79 100644
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@ -67,6 +67,9 @@ def authenticate(username=None, password=None, domain=None):
 
     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
@@ -126,6 +129,31 @@ def form_value_generate(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()
 
@@ -298,6 +326,21 @@ def ou_add(params={}):
 def ou_delete(params={}):
     return request('POST', 'ou.delete', 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))
 
@@ -308,7 +351,6 @@ def request(method, api_uri, get=None, post=None, headers={}):
         del response_data['status']
         return response_data['result']
     else:
-        print "ERROR: %r" % (response_data['reason'])
         return False
 
 def request_raw(method, api_uri, get=None, post=None, headers={}):
@@ -345,9 +387,111 @@ def request_raw(method, api_uri, get=None, post=None, headers={}):
 
     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():
     return request('GET', 'system.capabilities')
 
@@ -433,83 +577,6 @@ def user_form_value_generate(params=None):
 
     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 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_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_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_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 user_form_value_generate_uid(params=None):
     if params == None:
         params = get_user_input()
@@ -532,34 +599,8 @@ def user_info(user=None):
 
     return user
 
-def user_types_list():
-    return request('GET', 'user_types.list')
-
 def users_list(params={}):
     return request('POST', 'users.list', post=json.dumps(params))
 
-def resource_types_list():
-    return request('GET', 'resource_types.list')
-
-def resources_list():
-    return request('GET', 'resources.list')
-
-def resource_info(resource=None):
-    if resource == None:
-        resource = utils.ask_question("Resource DN")
-    return request('GET', 'resource.info', get={ 'id': resource })
-
-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 user_types_list():
+    return request('GET', 'user_types.list')


commit b50abf401de96dae1fa4a585eba6a135e1a39f11
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 15:09:06 2014 +0200

    Un-not-register the list-users command

diff --git a/pykolab/cli/commands.py b/pykolab/cli/commands.py
index 3139073..515d40c 100644
--- a/pykolab/cli/commands.py
+++ b/pykolab/cli/commands.py
@@ -55,7 +55,6 @@ def __init__():
 
     register('help', list_commands)
 
-    register('list_users', not_yet_implemented, description="Not yet implemented")
     register('delete_user', not_yet_implemented, description="Not yet implemented")
 
     register('list_groups', not_yet_implemented, description="Not yet implemented")


commit ae52e363418f07d5e485bcd9ba39ed8f25ba24d2
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 15:08:35 2014 +0200

    Do not issue a fatal error should deleting a mailbox not succeed

diff --git a/pykolab/cli/cmd_delete_mailbox.py b/pykolab/cli/cmd_delete_mailbox.py
index ce30c2b..ea64abc 100644
--- a/pykolab/cli/cmd_delete_mailbox.py
+++ b/pykolab/cli/cmd_delete_mailbox.py
@@ -57,5 +57,8 @@ def execute(*args, **kw):
         sys.exit(1)
 
     for delete_folder in delete_folders:
-        imap.delete_mailfolder(delete_folder)
+        try:
+            imap.delete_mailfolder(delete_folder)
+        except Exception, errmsg:
+            log.error(_("Could not delete mailbox '%s'") % (delete_folder))
 


commit 8d344ac056a6a987939ca65b4175e96ef7665b2c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 15:06:40 2014 +0200

    If the engine cannot be created (such as when running functional tests as a non-root user), create the cache in memory).

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 7249009..7bfccd7 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1414,7 +1414,7 @@ class LDAP(pykolab.base.Base):
         success = True
         for _type in ['user','group','role','sharedfolder']:
             try:
-                eval("self._change_delete_%s(entry, change)" % (_type))
+                eval("success = self._change_delete_%s(entry, change)" % (_type))
             except:
                 success = False
 
@@ -2243,7 +2243,7 @@ class LDAP(pykolab.base.Base):
             try:
                 entry['type'] = self._entry_type(entry)
             except:
-                entry['type'] = "unknown"
+                entry['type'] = None
 
             log.debug(_("Entry type: %s") % (entry['type']), level=8)
 
@@ -2317,14 +2317,6 @@ class LDAP(pykolab.base.Base):
 #
 #                    server = self.imap.user_mailbox_server(folder)
 
-        log.debug(
-                _("Done with _synchronize_callback() for entry %r") % (
-                        entry['id']
-                    ),
-                level=9
-            )
-
-
     def _unbind(self):
         """
             Discard the current set of bind credentials.
diff --git a/pykolab/auth/ldap/cache.py b/pykolab/auth/ldap/cache.py
index eaf2ffa..07c3d28 100644
--- a/pykolab/auth/ldap/cache.py
+++ b/pykolab/auth/ldap/cache.py
@@ -175,9 +175,13 @@ def init_db(domain,reinit=False):
 
     db_uri = 'sqlite:///%s/%s.db' % (KOLAB_LIB_PATH, domain)
     echo = conf.debuglevel > 8
-    engine = create_engine(db_uri, echo=echo)
 
-    metadata.create_all(engine)
+    try:
+        engine = create_engine(db_uri, echo=echo)
+        metadata.create_all(engine)
+    except:
+        engine = create_engine('sqlite://')
+        metadata.create_all(engine)
 
     Session = sessionmaker(bind=engine)
     db = Session()
@@ -185,14 +189,17 @@ def init_db(domain,reinit=False):
     return db
 
 def last_modify_timestamp(domain):
-    db = init_db(domain)
-    last_change = db.query(Entry).order_by(desc(Entry.last_change)).first()
-
     modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
     if modifytimestamp_format == None:
         modifytimestamp_format = "%Y%m%d%H%M%SZ"
 
-    if not last_change == None:
-        return last_change.last_change.strftime(modifytimestamp_format)
-
-    return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format)
+    try:
+        db = init_db(domain)
+        last_change = db.query(Entry).order_by(desc(Entry.last_change)).first()
+
+        if not last_change == None:
+            return last_change.last_change.strftime(modifytimestamp_format)
+        else:
+            return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format)
+    except:
+        return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format)


commit b00ea314f722fb2ba455e9a90bbd7f7616767946
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Jul 31 10:55:04 2014 +0200

    Ensure that UTF-7 is encoded as such

diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py
index 2f16e1f..f09e429 100644
--- a/pykolab/imap/cyrus.py
+++ b/pykolab/imap/cyrus.py
@@ -384,9 +384,9 @@ class Cyrus(cyruslib.CYRUS):
                 verify_folder_search = "%s@%s" % (verify_folder_search, mbox['domain'])
 
             if ' ' in verify_folder_search:
-                folders = self.lm('"%s"' % verify_folder_search)
+                folders = self.lm('"%s"' % self.folder_utf7(verify_folder_search))
             else:
-                folders = self.lm(verify_folder_search)
+                folders = self.lm(self.folder_utf7(verify_folder_search))
 
             # NOTE: Case also covered is valid hexadecimal folders; won't be the
             # actual check as intended, but doesn't give you anyone else's data


commit 5aa6b400ee6ed8f5293aa33427fb769b43736907
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 30 14:24:49 2014 -0400

    Read/write comment property for event objects (#3186)

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 98436d9..75b00ac 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -322,6 +322,12 @@ class Event(object):
     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():
@@ -601,6 +607,10 @@ class Event(object):
     def set_description(self, description):
         self.event.setDescription(str(description))
 
+    def set_comment(self, comment):
+        if hasattr(self.event, 'setComment'):
+            self.event.setComment(str(comment))
+
     def set_dtstamp(self, _datetime):
         self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
 


commit 46fd1e032160f010525bddedf18a9ccd14a5c62a
Merge: e949ef9 bc7fba1
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Jul 30 14:24:57 2014 +0200

    Merge branch 'master' of ssh://git.kolabsys.com/git/pykolab



commit e949ef9342ca4c8b67d6207eb2ac211e0b1c2122
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Jul 30 14:24:37 2014 +0200

    Add a 'list-ous' command

diff --git a/pykolab/cli/cmd_list_ous.py b/pykolab/cli/cmd_list_ous.py
new file mode 100644
index 0000000..670b609
--- /dev/null
+++ b/pykolab/cli/cmd_list_ous.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com)
+#
+# Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen a kolabsys.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+import commands
+
+import pykolab
+
+from pykolab import utils
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.cli')
+conf = pykolab.getConf()
+
+def __init__():
+    commands.register('list_ous', execute, description="List organizational units.")
+
+def execute(*args, **kw):
+    from pykolab import wap_client
+
+    wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw"))
+
+    ous = wap_client.ous_list()
+    print '\n'.join(ous['list'].keys())


commit eedee64b0811fb9e95417feaf851cb9cb19a280a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Jul 30 14:24:07 2014 +0200

    Add some basic ou functions to the wap client

diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index eabc893..299b847 100644
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@ -69,6 +69,7 @@ def authenticate(username=None, password=None, domain=None):
 
     if response.has_key('session_token'):
         session_id = response['session_token']
+        return True
 
 def connect():
     global conn
@@ -288,8 +289,17 @@ def group_members_list(group=None):
 def group_types_list():
     return request('GET', 'group_types.list')
 
-def groups_list():
-    return request('GET', 'groups.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 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)
@@ -525,8 +535,8 @@ def user_info(user=None):
 def user_types_list():
     return request('GET', 'user_types.list')
 
-def users_list():
-    return request('GET', 'users.list')
+def users_list(params={}):
+    return request('POST', 'users.list', post=json.dumps(params))
 
 def resource_types_list():
     return request('GET', 'resource_types.list')


commit bae16970b5d9d5379214658397834499fb3755d8
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Jul 30 14:23:36 2014 +0200

    Add the traceback to the error log as well

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index d1a0b2d..7249009 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -2704,9 +2704,13 @@ class LDAP(pykolab.base.Base):
 
             except Exception, errmsg:
                 log.error(_("An error occured using %s: %r") % (supported_control, errmsg))
+                import traceback
+
                 if conf.debuglevel > 8:
-                    import traceback
                     traceback.print_exc()
+
+                log.error(_("%s") % (traceback.format_exc()))
+
                 continue
 
         return _results


commit bc7fba1f1f1e0afab921b069be6e7d9c562addda
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Jul 29 15:02:33 2014 +0200

    Always call connect(), let it figure out whether it is already connected or not

diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index 33984bf..6cec86f 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -211,8 +211,7 @@ class IMAP(object):
         folder_path = self.folder_utf7(folder_path)
 
         if not server == None:
-            if not self._imap.has_key(server):
-                self.connect(server=server)
+            self.connect(server=server)
 
             try:
                 self._imap[server].cm(folder_path, partition=partition)


commit 082464b7e59451a2c993a9851de117c979cc24af
Merge: 063eb95 5248493
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sun Jul 27 16:28:03 2014 +0200

    Merge branch 'master' of ssh://git.kolabsys.com/git/pykolab



commit 063eb95d66064f8092a5ad918842e33cba9b8f36
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sun Jul 27 14:17:49 2014 +0200

    Ensure the correct format for the modifytimestamp_format is included in the default configuration, and it's obtained as a raw value

diff --git a/conf/kolab.conf b/conf/kolab.conf
index 2f8ea2b..d9a6a5a 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -234,6 +234,10 @@ domain_rootdn_attribute = inetdomainbasedn
 
 ; The attribute that holds the quota.
 quota_attribute = mailquota
+
+; The format of the modifytimestamp attribute values
+modifytimestamp_format = %Y%m%d%H%M%SZ
+
 ; A unique attribute that can be used to identify the entry beyond renames and
 ; moves. Note that 'nsuniqueid' is specific to all Netscape-based directory
 ; services.
diff --git a/pykolab/auth/ldap/cache.py b/pykolab/auth/ldap/cache.py
index c21d9a6..eaf2ffa 100644
--- a/pykolab/auth/ldap/cache.py
+++ b/pykolab/auth/ldap/cache.py
@@ -63,7 +63,7 @@ class Entry(object):
         self.uniqueid = uniqueid
         self.result_attribute = result_attr
 
-        modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format')
+        modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
         if modifytimestamp_format == None:
             modifytimestamp_format = "%Y%m%d%H%M%SZ"
 
@@ -95,7 +95,7 @@ mapper(Entry, entry_table)
 ##
 
 def delete_entry(domain, entry):
-    result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+    result_attribute = conf.get_raw('cyrus-sasl', 'result_attribute')
 
     db = init_db(domain)
     _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
@@ -105,7 +105,7 @@ def delete_entry(domain, entry):
         db.commit()
 
 def get_entry(domain, entry, update=True):
-    result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+    result_attribute = conf.get_raw('cyrus-sasl', 'result_attribute')
 
     _entry = None
 
@@ -139,7 +139,7 @@ def get_entry(domain, entry, update=True):
         db.commit()
         _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
     else:
-        modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format')
+        modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
         if modifytimestamp_format == None:
             modifytimestamp_format = "%Y%m%d%H%M%SZ"
 
@@ -188,7 +188,7 @@ def last_modify_timestamp(domain):
     db = init_db(domain)
     last_change = db.query(Entry).order_by(desc(Entry.last_change)).first()
 
-    modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format')
+    modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format')
     if modifytimestamp_format == None:
         modifytimestamp_format = "%Y%m%d%H%M%SZ"
 


commit a60e0128a79fa02add67c517538946a772deaf23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jul 25 23:09:54 2014 -0400

    Finish dump of event alarms as dict

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 8e41a92..98436d9 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -7,6 +7,7 @@ import pytz
 import time
 import uuid
 import base64
+import re
 
 import pykolab
 from pykolab import constants
@@ -56,6 +57,17 @@ class Event(object):
             "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",
@@ -494,8 +506,7 @@ class Event(object):
         if status in self.status_map.keys():
             return status
 
-        if status in self.status_map.values():
-            return [k for k, v in self.status_map.iteritems() if v == status][0]
+        return self._translate_value(status, self.status_map)
 
     def get_ical_sequence(self):
         return str(self.event.sequence()) if self.event.sequence() else None
@@ -839,7 +850,7 @@ class Event(object):
             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 = [dict(type=x.type()) for x in val]
+                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')]
 
@@ -848,6 +859,36 @@ class Event(object):
 
         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):
         from email.MIMEMultipart import MIMEMultipart
         from email.MIMEBase import MIMEBase
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 5017091..f069be3 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -190,6 +190,50 @@ xml_event = """
             <value>BUSY</value>
           </x-custom>
         </properties>
+        <components>
+          <valarm>
+            <properties>
+              <action>
+                <text>DISPLAY</text>
+              </action>
+              <description>
+                <text>alarm 1</text>
+              </description>
+              <trigger>
+                <parameters>
+                  <related>
+                    <text>START</text>
+                  </related>
+                </parameters>
+                <duration>-PT2H</duration>
+              </trigger>
+            </properties>
+          </valarm>
+          <valarm>
+            <properties>
+              <action>
+                <text>EMAIL</text>
+              </action>
+              <summary>
+                <text>test</text>
+              </summary>
+              <description>
+                <text>alarm 2</text>
+              </description>
+              <attendee>
+                  <cal-address>mailto:%3Cjohn.die%40example.org%3E</cal-address>
+              </attendee>
+              <trigger>
+                <parameters>
+                  <related>
+                    <text>START</text>
+                  </related>
+                </parameters>
+                <duration>-P1D</duration>
+              </trigger>
+            </properties>
+          </valarm>
+        </components>
       </vevent>
     </components>
   </vcalendar>
@@ -520,6 +564,13 @@ END:VEVENT
         self.assertEqual(data['rrule']['wkst'], 'MO')
         self.assertIsInstance(data['rrule']['until'], datetime.date)
 
+        self.assertIsInstance(data['alarm'], list)
+        self.assertEqual(len(data['alarm']), 2)
+        self.assertEqual(data['alarm'][0]['action'], 'DISPLAY')
+        self.assertEqual(data['alarm'][1]['action'], 'EMAIL')
+        self.assertEqual(data['alarm'][1]['trigger']['value'], '-P1D')
+        self.assertEqual(len(data['alarm'][1]['attendee']), 1)
+
 
 if __name__ == '__main__':
     unittest.main()


commit f29b67ff6973447de96eeeb465884fc1a51c49ef
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 29 09:44:42 2014 +0200

    Fix dict conversion of recurrence rules

diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py
index eb17fd5..4a0b6c5 100644
--- a/pykolab/xml/recurrence_rule.py
+++ b/pykolab/xml/recurrence_rule.py
@@ -60,6 +60,7 @@ class RecurrenceRule(kolabformat.RecurrenceRule):
         'until':     'end',
         'bymonth':   'bymonth',
         'byday':     'byday',
+        'bymonthday':'bymonthday',
         'byyearday': 'byyearday',
         'byweekno':  'byweekno',
         'byhour':    'byhour',
@@ -106,9 +107,9 @@ class RecurrenceRule(kolabformat.RecurrenceRule):
             if isinstance(val, kolabformat.cDateTime):
                 val = xmlutils.from_cdatetime(val, True)
             elif isinstance(val, kolabformat.vectori):
-                val = [int(v) for x in val]
+                val = ",".join([int(v) for x in val])
             elif isinstance(val, kolabformat.vectordaypos):
-                val = ["%d%s" % (x.occurence, self._translate_value(x.weekday)) for x in val]
+                val = ",".join(["%s%s" % (str(x.occurence()) if x.occurence() != 0 else '', self._translate_value(x.weekday(), self.weekday_map)) for x in val])
             if val is not None:
                 data[p] = val
 


commit 524849338fcb0cb40bcdb18f4dbe7e9660074f20
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 23 08:23:49 2014 -0400

    Add methods to dump Kolab XML objects as dict()

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 17da24e..816ee1d 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -144,6 +144,8 @@ def check_event_conflict(kolab_event, itip_event):
     if kolab_event.uid == itip_event['uid']:
         return conflict
 
+    # TODO: don't consider conflict if event has TRANSP:TRANSPARENT
+
     _es = to_dt(kolab_event.get_start())
     _ee = to_dt(kolab_event.get_ical_dtend())  # use iCal style end date: next day for all-day events
 
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 2bb42d4..3e12716 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -4,6 +4,7 @@ from attendee import participant_status_label
 
 from contact import Contact
 from contact_reference import ContactReference
+from recurrence_rule import RecurrenceRule
 
 from event import Event
 from event import EventIntegrityError
@@ -19,6 +20,7 @@ __all__ = [
         "Contact",
         "ContactReference",
         "Event",
+        "RecurrenceRule",
         "event_from_ical",
         "event_from_string",
         "to_dt",
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 5d469c2..7921280 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -56,6 +56,13 @@ class Attendee(kolabformat.Attendee):
             "FALSE": False,
         }
 
+    properties_map = {
+            'role': 'get_role',
+            'rsvp':  'rsvp',
+            'partstat':  'get_participant_status',
+            'cutype':   'get_cutype',
+        }
+
     def __init__(
             self,
             email,
@@ -97,6 +104,12 @@ class Attendee(kolabformat.Attendee):
         if not participant_status == None:
             self.set_participant_status(participant_status)
 
+    def copy_from(self, obj):
+        if isinstance(obj, kolabformat.Attendee):
+            kolabformat.Attendee.__init__(self, obj)
+            self.contactreference = ContactReference(obj.contact())
+            self.email = self.contactreference.get_email()
+
     def delegate_from(self, delegators):
         crefs = []
 
@@ -138,8 +151,11 @@ class Attendee(kolabformat.Attendee):
 
         self.setDelegatedTo(list(set(crefs)))
 
-    def get_cutype(self):
-        return self.cutype()
+    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):
         return self.delegatedFrom()
@@ -161,16 +177,22 @@ class Attendee(kolabformat.Attendee):
     def get_participant_status(self, translated=False):
         partstat = self.partStat()
         if translated:
-            partstat_name_map = dict([(v, k) for (k, v) in self.participant_status_map.iteritems()])
-            return partstat_name_map[partstat] if partstat_name_map.has_key(partstat) else 'UNKNOWN'
+            return self._translate_value(partstat, self.participant_status_map)
         return partstat
 
-    def get_role(self):
-        return self.role()
+    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():
             self.setCutype(self.cutype_map[cutype])
@@ -202,6 +224,22 @@ class Attendee(kolabformat.Attendee):
     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
 
diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
index 1577b58..9a2c103 100644
--- a/pykolab/xml/contact.py
+++ b/pykolab/xml/contact.py
@@ -39,5 +39,9 @@ class Contact(kolabformat.Contact):
     def set_name(self, name):
         self.setName(name)
 
+    def to_ditc(self):
+        # TODO: implement this
+        return dict(name=self.name())
+
     def __str__(self):
         return kolabformat.writeContact(self)
diff --git a/pykolab/xml/contact_reference.py b/pykolab/xml/contact_reference.py
index 0d6dec5..5a832da 100644
--- a/pykolab/xml/contact_reference.py
+++ b/pykolab/xml/contact_reference.py
@@ -11,9 +11,18 @@ import kolabformat
 """
 
 class ContactReference(kolabformat.ContactReference):
+    properties_map = {
+        'email': 'email',
+        'name':  'name',
+        'type':  'type',
+        'uid':   'uid',
+    }
+
     def __init__(self, email=None):
         if email == None:
             kolabformat.ContactReference.__init__(self)
+        elif isinstance(email, kolabformat.ContactReference):
+            kolabformat.ContactReference.__init__(self, email.email(), email.name(), email.uid())
         else:
             kolabformat.ContactReference.__init__(self, email)
 
@@ -31,3 +40,15 @@ class ContactReference(kolabformat.ContactReference):
 
     def set_name(self, name):
         self.setName(name)
+
+    def to_dict(self):
+        data = dict()
+
+        for p, getter in self.properties_map.iteritems():
+            val = None
+            if hasattr(self, getter):
+                val = getattr(self, getter)()
+            if val is not None:
+                data[p] = val
+
+        return data
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 4ac4997..8e41a92 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -18,6 +18,7 @@ from pykolab.translate import _
 from os import path
 from attendee import Attendee
 from contact_reference import ContactReference
+from recurrence_rule import RecurrenceRule
 
 log = pykolab.getLogger('pykolab.xml_event')
 
@@ -55,6 +56,37 @@ class Event(object):
             "CONFIDENTIAL": kolabformat.ClassConfidential,
         }
 
+    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_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 = []
@@ -271,7 +303,7 @@ class Event(object):
 
     def get_created(self):
         try:
-            return xmlutils.from_cdatetime(self.event.created(), False)
+            return xmlutils.from_cdatetime(self.event.created(), True)
         except ValueError:
             return datetime.datetime.now()
 
@@ -479,7 +511,7 @@ class Event(object):
         except:
             self.__str__()
 
-        return xmlutils.from_cdatetime(self.event.lastModified(), False)
+        return xmlutils.from_cdatetime(self.event.lastModified(), True)
 
     def get_organizer(self):
         organizer = self.event.organizer()
@@ -780,6 +812,42 @@ class Event(object):
         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 = [dict(type=x.type()) 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 to_message(self):
         from email.MIMEMultipart import MIMEMultipart
         from email.MIMEBase import MIMEBase
diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py
new file mode 100644
index 0000000..eb17fd5
--- /dev/null
+++ b/pykolab/xml/recurrence_rule.py
@@ -0,0 +1,117 @@
+import kolabformat
+from pykolab.xml import utils as xmlutils
+
+"""
+    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)
+"""
+
+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 = {
+        'frequency': 'get_frequency',
+        'interval':  'interval',
+        'count':     'count',
+        'until':     'end',
+        'bymonth':   'bymonth',
+        'byday':     'byday',
+        '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 get_frequency(self, translated=False):
+        freq = self.frequency()
+        if translated:
+            return self._translate_value(freq, self.frequency_map)
+        return freq
+
+    def get_weekstart(self, translated=False):
+        wkst = self.weekStart()
+        if translated:
+            return self._translate_value(wkst, self.weekday_map)
+        return wkst
+
+    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() 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(v) for x in val]
+            elif isinstance(val, kolabformat.vectordaypos):
+                val = ["%d%s" % (x.occurence, self._translate_value(x.weekday)) for x in val]
+            if val is not None:
+                data[p] = val
+
+        return data
+
+
diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py
index 8bcee3c..d7584e3 100644
--- a/tests/unit/test-002-attendee.py
+++ b/tests/unit/test-002-attendee.py
@@ -108,5 +108,25 @@ class TestEventXML(unittest.TestCase):
         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 at doe.org')
+        self.assertTrue(data['rsvp'])
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 2c5a478..5017091 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -65,6 +65,136 @@ END:VALARM
 END:VEVENT
 """
 
+xml_event = """
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+  <vcalendar>
+    <properties>
+      <prodid>
+        <text>Libkolabxml-1.1</text>
+      </prodid>
+      <version>
+        <text>2.0</text>
+      </version>
+      <x-kolab-version>
+        <text>3.1.0</text>
+      </x-kolab-version>
+    </properties>
+    <components>
+      <vevent>
+        <properties>
+          <uid>
+            <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text>
+          </uid>
+          <created>
+            <date-time>2014-07-07T01:28:23Z</date-time>
+          </created>
+          <dtstamp>
+            <date-time>2014-07-07T01:28:23Z</date-time>
+          </dtstamp>
+          <sequence>
+            <integer>1</integer>
+          </sequence>
+          <class>
+            <text>PUBLIC</text>
+          </class>
+          <dtstart>
+            <parameters>
+              <tzid>
+                <text>/kolab.org/Europe/London</text>
+              </tzid>
+            </parameters>
+            <date-time>2014-08-13T10:00:00</date-time>
+          </dtstart>
+          <dtend>
+            <parameters>
+              <tzid><text>/kolab.org/Europe/London</text></tzid>
+            </parameters>
+            <date-time>2014-08-13T14:00:00</date-time>
+          </dtend>
+          <rrule>
+            <recur>
+              <freq>DAILY</freq>
+              <until>
+                <date>2014-07-25</date>
+              </until>
+            </recur>
+          </rrule>
+          <exdate>
+            <parameters>
+              <tzid>
+                <text>/kolab.org/Europe/Berlin</text>
+              </tzid>
+            </parameters>
+            <date>2014-07-19</date>
+            <date>2014-07-26</date>
+            <date>2014-07-12</date>
+            <date>2014-07-13</date>
+            <date>2014-07-20</date>
+            <date>2014-07-27</date>
+            <date>2014-07-05</date>
+            <date>2014-07-06</date>
+          </exdate>
+          <summary>
+            <text>test</text>
+          </summary>
+          <description>
+            <text>test</text>
+          </description>
+          <priority>
+            <integer>5</integer>
+          </priority>
+          <status>
+            <text>CANCELLED</text>
+          </status>
+          <location>
+            <text>Room 101</text>
+          </location>
+          <organizer>
+            <parameters>
+              <cn><text>Doe, John</text></cn>
+            </parameters>
+            <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
+          </organizer>
+          <attendee>
+            <parameters>
+              <partstat><text>ACCEPTED</text></partstat>
+              <role><text>REQ-PARTICIPANT</text></role>
+              <rsvp><boolean>true</boolean></rsvp>
+            </parameters>
+            <cal-address>mailto:%3Cjane%40example.org%3E</cal-address>
+          </attendee>
+          <attendee>
+            <parameters>
+              <partstat><text>TENTATIVE</text></partstat>
+              <role><text>OPT-PARTICIPANT</text></role>
+            </parameters>
+            <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address>
+          </attendee>
+          <attach>
+            <parameters>
+              <fmttype>
+                <text>text/html</text>
+              </fmttype>
+              <x-label>
+                <text>noname.1395223627.5555</text>
+              </x-label>
+            </parameters>
+            <uri>cid:noname.1395223627.5555</uri>
+          </attach>
+          <x-custom>
+            <identifier>X-MOZ-RECEIVED-DTSTAMP</identifier>
+            <value>20140224T155612Z</value>
+          </x-custom>
+          <x-custom>
+            <identifier>X-GWSHOW-AS</identifier>
+            <value>BUSY</value>
+          </x-custom>
+        </properties>
+      </vevent>
+    </components>
+  </vcalendar>
+</icalendar>
+"""
 
 class TestEventXML(unittest.TestCase):
     event = Event()
@@ -181,7 +311,7 @@ METHOD:REQUEST
         event = event_from_ical(ical.walk('VEVENT')[0].to_ical())
 
         self.assertEqual(event.get_location(), "Location")
-        self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11")
+        self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11+00:00")
         self.assertEqual(event.get_description(), "Description\n2 lines")
         self.assertEqual(event.get_url(), "http://somelink.com/foo")
         self.assertEqual(event.get_transparency(), False)
@@ -310,83 +440,7 @@ END:VEVENT
         self.assertEqual(self.event.get_last_occurrence(), None)
 
     def test_022_load_from_xml(self):
-        xml = """
-<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
-  <vcalendar>
-    <properties>
-      <prodid>
-        <text>Libkolabxml-1.1</text>
-      </prodid>
-      <version>
-        <text>2.0</text>
-      </version>
-      <x-kolab-version>
-        <text>3.1.0</text>
-      </x-kolab-version>
-    </properties>
-    <components>
-      <vevent>
-        <properties>
-          <uid>
-            <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text>
-          </uid>
-          <created>
-            <date-time>2014-07-07T01:28:23Z</date-time>
-          </created>
-          <dtstamp>
-            <date-time>2014-07-07T01:28:23Z</date-time>
-          </dtstamp>
-          <sequence>
-            <integer>1</integer>
-          </sequence>
-          <class>
-            <text>PUBLIC</text>
-          </class>
-          <dtstart>
-            <parameters>
-              <tzid>
-                <text>/kolab.org/Europe/London</text>
-              </tzid>
-            </parameters>
-            <date-time>2014-08-13T10:00:00</date-time>
-          </dtstart>
-          <dtend>
-            <parameters>
-              <tzid><text>/kolab.org/Europe/London</text></tzid>
-            </parameters>
-            <date-time>2014-08-13T14:00:00</date-time>
-          </dtend>
-          <summary>
-            <text>test</text>
-          </summary>
-          <organizer>
-            <parameters>
-              <cn><text>Doe, John</text></cn>
-            </parameters>
-            <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
-          </organizer>
-          <attendee>
-            <parameters>
-              <partstat><text>ACCEPTED</text></partstat>
-              <role><text>REQ-PARTICIPANT</text></role>
-              <rsvp><boolean>true</boolean></rsvp>
-            </parameters>
-            <cal-address>mailto:%3Cjane%40example.org%3E</cal-address>
-          </attendee>
-          <attendee>
-            <parameters>
-              <partstat><text>TENTATIVE</text></partstat>
-              <role><text>OPT-PARTICIPANT</text></role>
-            </parameters>
-            <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address>
-          </attendee>
-        </properties>
-      </vevent>
-    </components>
-  </vcalendar>
-</icalendar>
-"""
-        event = event_from_string(xml)
+        event = event_from_string(xml_event)
         self.assertEqual(event.uid, '75c740bb-b3c6-442c-8021-ecbaeb0a025e')
         self.assertEqual(event.get_attendee_by_email("jane at example.org").get_participant_status(), kolabformat.PartAccepted)
         self.assertEqual(event.get_sequence(), 1)
@@ -432,6 +486,40 @@ END:VEVENT
         event = event_from_ical(vevent)
         self.assertRaises(EventIntegrityError, event.to_message)
 
+    def test_025_to_dict(self):
+        data = event_from_string(xml_event).to_dict()
+
+        self.assertIsInstance(data, dict)
+        self.assertIsInstance(data['start'], datetime.datetime)
+        self.assertIsInstance(data['end'], datetime.datetime)
+        self.assertIsInstance(data['created'], datetime.datetime)
+        self.assertIsInstance(data['lastmodified-date'], datetime.datetime)
+        self.assertEqual(data['uid'], '75c740bb-b3c6-442c-8021-ecbaeb0a025e')
+        self.assertEqual(data['summary'], 'test')
+        self.assertEqual(data['location'], 'Room 101')
+        self.assertEqual(data['description'], 'test')
+        self.assertEqual(data['priority'], 5)
+        self.assertEqual(data['status'], 'CANCELLED')
+        self.assertEqual(data['sequence'], 1)
+        self.assertEqual(data['transparency'], False)
+        self.assertEqual(data['X-GWSHOW-AS'], 'BUSY')
+
+        self.assertIsInstance(data['organizer'], dict)
+        self.assertEqual(data['organizer']['email'], 'john at example.org')
+
+        self.assertEqual(len(data['attendee']), 2)
+        self.assertIsInstance(data['attendee'][0], dict)
+
+        self.assertEqual(len(data['attach']), 1)
+        self.assertIsInstance(data['attach'][0], dict)
+        self.assertEqual(data['attach'][0]['fmttype'], 'text/html')
+
+        self.assertIsInstance(data['rrule'], dict)
+        self.assertEqual(data['rrule']['frequency'], 'DAILY')
+        self.assertEqual(data['rrule']['interval'], 1)
+        self.assertEqual(data['rrule']['wkst'], 'MO')
+        self.assertIsInstance(data['rrule']['until'], datetime.date)
+
 
 if __name__ == '__main__':
     unittest.main()


commit b3e6648328dd00dd53e60633b23a40ed6ff578e5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 22 10:03:03 2014 -0400

    Test localized notifications from iTip messages

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index a01d955..5d469c2 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -22,7 +22,7 @@ participant_status_labels = {
     }
 
 def participant_status_label(status):
-    return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else status
+    return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else _(status)
 
 
 class Attendee(kolabformat.Attendee):
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index dd419f0..1af22ff 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -178,14 +178,14 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
         }
 
-        self.bob = {
-            'displayname': 'Bob Auto',
-            'mail': 'bob.auto at example.org',
-            'dn': 'uid=auto,ou=People,dc=example,dc=org',
-            'preferredlanguage': 'en_US',
-            'mailbox': 'user/bob.auto at example.org',
-            'kolabtargetfolder': 'user/bob.auto/Calendar at example.org',
-            'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE']
+        self.mark = {
+            'displayname': 'Mark German',
+            'mail': 'mark.german at example.org',
+            'dn': 'uid=german,ou=People,dc=example,dc=org',
+            'preferredlanguage': 'de_DE',
+            'mailbox': 'user/mark.german at example.org',
+            'kolabtargetfolder': 'user/mark.german/Calendar at example.org',
+            'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE_AND_NOTIFY']
         }
 
         self.external = {
@@ -197,7 +197,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage'])
         user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage'])
         user_add("Jack", "Tentative", kolabinvitationpolicy=self.jack['kolabinvitationpolicy'], preferredlanguage=self.jack['preferredlanguage'])
-        user_add("Bob", "Auto", kolabinvitationpolicy=self.bob['kolabinvitationpolicy'], preferredlanguage=self.bob['preferredlanguage'])
+        user_add("Mark", "German", kolabinvitationpolicy=self.mark['kolabinvitationpolicy'], preferredlanguage=self.mark['preferredlanguage'])
 
         time.sleep(1)
         from tests.functional.synchronize import synchronize_once
@@ -606,18 +606,18 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.purge_mailbox(self.john['mailbox'])
 
         start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
-        uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.bob, self.jack])
+        uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.mark, self.jack])
 
         # send a reply from jane to john
         self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
 
         # check for notification message
-        # this notification should be suppressed until bob has replied, too
+        # this notification should be suppressed until mark has replied, too
         notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
         self.assertEqual(notification, None)
 
-        # send a reply from bob to john
-        self.send_itip_reply(uid, self.bob['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+        # send a reply from mark to john
+        self.send_itip_reply(uid, self.mark['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
 
         notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
         self.assertIsInstance(notification, email.message.Message)
@@ -628,7 +628,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         self.purge_mailbox(self.john['mailbox'])
 
-        # send a reply from bob to john
+        # send a reply from mark to john
         self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
 
         # this triggers an additional notification
@@ -639,6 +639,28 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertNotIn(_("PENDING"), notification_text)
 
 
+    def test_008_notify_translated(self):
+        self.purge_mailbox(self.mark['mailbox'])
+
+        start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.mark, attendees=[self.jane])
+
+        # send a reply from jane to mark
+        self.send_itip_reply(uid, self.jane['mail'], self.mark['mail'], start=start)
+
+        # change translations to de_DE
+        pykolab.translate.setUserLanguage(self.mark['preferredlanguage'])
+        notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.mark['mail'], self.mark['mailbox'])
+        self.assertIsInstance(notification, email.message.Message)
+
+        notification_text = str(notification.get_payload());
+        self.assertIn(self.jane['mail'], notification_text)
+        self.assertIn(participant_status_label("ACCEPTED")+":", notification_text)
+
+        # reset localization
+        pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
+
     def test_009_outdated_reply(self):
         self.purge_mailbox(self.john['mailbox'])
 
@@ -711,3 +733,5 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
         self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
         self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+
+
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 03585ee..de29450 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -144,6 +144,9 @@ def cleanup():
 def execute(*args, **kw):
     global auth, imap
 
+    # (re)set language to default
+    pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
     if not os.path.isdir(mybasepath):
         os.makedirs(mybasepath)
 
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index dba2653..b31a8d0 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -100,6 +100,9 @@ def cleanup():
 def execute(*args, **kw):
     global auth, imap
 
+    # (re)set language to default
+    pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
     if not os.path.isdir(mybasepath):
         os.makedirs(mybasepath)
 
@@ -875,6 +878,8 @@ def send_response(from_address, itip_events, owner=None):
         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")
 
@@ -944,6 +949,10 @@ def send_owner_notification(resource, owner, itip_event, success=True):
             level=8
         )
 
+        # change gettext language to the preferredlanguage setting of the resource owner
+        if owner.has_key('preferredlanguage'):
+            pykolab.translate.setUserLanguage(owner['preferredlanguage'])
+
         message_text = owner_notification_text(resource, owner, itip_event['xml'], success)
 
         msg = MIMEText(utils.stripped_message(message_text))


commit 9876593d6551fe4e7dcd7ce93011f70ebab5b8a1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 22 09:08:36 2014 -0400

    Updated localization index and some German texts

diff --git a/po/POTFILES.in b/po/POTFILES.in
index 8109c28..5a5bc37 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -12,11 +12,10 @@ ext/python/Tools/freeze/makefreeze.py
 ext/python/Tools/freeze/makemakefile.py
 ext/python/Tools/freeze/parsesetup.py
 ext/python/Tools/freeze/winmakemakefile.py
+kolab-cli.py
 kolabd/__init__.py
 kolabd/process.py
 kolabd.py
-kolab/__init__.py
-kolab.py
 pykolab/auth/__init__.py
 pykolab/auth/ldap/auth_cache.py
 pykolab/auth/ldap/cache.py
@@ -84,11 +83,13 @@ pykolab/imap/cyrus.py
 pykolab/imap/__init__.py
 pykolab/imap_utf7.py
 pykolab/__init__.py
+pykolab/itip/__init__.py
 pykolab/logger.py
 pykolab/plugins/defaultfolders/__init__.py
 pykolab/plugins/dynamicquota/__init__.py
 pykolab/plugins/__init__.py
 pykolab/plugins/recipientpolicy/__init__.py
+pykolab/plugins/roundcubedb/__init__.py
 pykolab/plugins/sievemgmt/__init__.py
 pykolab/setup/components.py
 pykolab/setup/__init__.py
@@ -137,6 +138,8 @@ tests/functional/test_wallace/test_003_nonascii_subject.py
 tests/functional/test_wallace/test_004_nonascii_addresses.py
 tests/functional/test_wallace/test_005_resource_add.py
 tests/functional/test_wallace/test_005_resource_invitation.py
+tests/functional/test_wallace/test_006_resource_performance.py
+tests/functional/test_wallace/test_007_invitationpolicy.py
 tests/functional/test_wap_client/__init__.py
 tests/functional/test_wap_client/test_001_connect.py
 tests/functional/test_wap_client/test_002_user_add.py
@@ -160,12 +163,18 @@ tests/unit/test-007-ldap_syncrepl.py
 tests/unit/test-008-sievelib.py
 tests/unit/test-009-parse_ldap_uri.py
 tests/unit/test-010-transliterate.py
+tests/unit/test-011-itip.py
 tests/unit/test-011-wallace_resources.py
+tests/unit/test-012-wallace_invitationpolicy.py
+tests/unit/test-014-conf-and-raw.py
+tests/unit/test-015-translate.py
 test-wallace.py
 ucs/kolab_sieve.py
 ucs/listener.py
 wallace/__init__.py
 wallace/module_footer.py
+wallace/module_gpgencrypt.py
+wallace/module_invitationpolicy.py
 wallace/module_optout.py
 wallace/module_resources.py
 wallace/modules.py
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index b2ac963..1966fae 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1,214 +1,70 @@
-activate_domain.py
-bin/test_parse_policy.py
-bin/test-read-input.py
-bonnie/__init__.py
-bonnie.py
-demo.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/about.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/authentication.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/data.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/environ.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/error.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/index.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/login.mak.py
-kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/master.mak.py
-kwap/ez_setup/__init__.py
-kwap/kwap/config/app_cfg.py
-kwap/kwap/config/environment.py
-kwap/kwap/config/__init__.py
-kwap/kwap/config/middleware.py
-kwap/kwap/controllers/domain.py
-kwap/kwap/controllers/error.py
-kwap/kwap/controllers/__init__.py
-kwap/kwap/controllers/root.py
-kwap/kwap/controllers/secure.py
-kwap/kwap/controllers/template.py
-kwap/kwap/grids/domains.py
-kwap/kwap/grids/__init__.py
-kwap/kwap/__init__.py
-kwap/kwap/lib/app_globals.py
-kwap/kwap/lib/base.py
-kwap/kwap/lib/helpers.py
-kwap/kwap/lib/__init__.py
-kwap/kwap/model/auth.py
-kwap/kwap/model/__init__.py
-kwap/kwap/templates/domain/__init__.py
-kwap/kwap/templates/__init__.py
-kwap/kwap/tests/functional/__init__.py
-kwap/kwap/tests/functional/test_authentication.py
-kwap/kwap/tests/functional/test_root.py
-kwap/kwap/tests/__init__.py
-kwap/kwap/tests/models/__init__.py
-kwap/kwap/tests/models/test_auth.py
-kwap/kwap/websetup/bootstrap.py
-kwap/kwap/websetup/__init__.py
-kwap/kwap/websetup/schema.py
-kwap/setup.py
-munich_demo.py
-play/anon-imap/anon-imap.py
-play/augeas-insert.py
-play/base_64_decode.py
-play/brepr_vs_unicode.py
-play/cleanup_acls.py
-play/cliconfmgmt.py
-play/conf-add-file-to-service.py
-play/conf-add-setting.py
-play/conf-list-config-files.py
-play/confmgmt/augeas.py
-play/confmgmt/db.py
-play/confmgmt/__init__.py
-play/confmgmt/model.py
-play/conf.py
-play/conf-settings-from-file.py
-play/conf-update-file.py
-play/detect-object-type.py
-play/dttz.py
-play/effectiverights.py
-play/flawed_zpush_testing_create_folders.py
-play/fork.py
-play/get_uid.py
-play/imap_annotations_test.py
-play/kolab-sap/kolab_smtp_access_policy.py
-play/libkolabxml/contact.py
-play/libkolabxml/event_imap.py
-play/libkolabxml/event.py
-play/libkolabxml/event_rfc822.py
-play/libkolabxml/todo.py
-play/load_test.py
-play/migrate_lowercase_uid.py
-play/migrate_mail_to_uid_prod.py
-play/migrate_mail_to_uid.py
-play/migrate_uid_to_mail_prod.py
-play/migrate_uid_to_mail.py
-play/noheaderini.py
-play/not-an-itip-message.py
-play/openssl/license.py
-play/parallel_persistent_searches.py
-play/parse_policy.py
-play/persistent_search_kolab_23.py
-play/persistent_search.py
-play/pooling.py
-play/purge_users_roundcube.py
-play/push_contacts.py
-play/pygpgme/sign.py
-play/pygpgme/verify.py
-play/regexps.py
-play/rolequota/__init__.py
-play/roundcube_database/identities.py
-play/roundcube_database/__init__.py
-play/roundcube_database.py
-play/roundcube_database/users.py
-play/split_message_file.py
-play/sqlalchemy_schemadisplay.py
-play/strip_many_headers.py
-play/sync_client.py
-play/sync_repl_kolab_23.py
-play/test_augeas_load.py
-play/test_cal_spread.py
-play/test-entitlements.py
-play/test_filter.py
-play/test_folders.py
-play/test-icalendar-attendee.py
-play/test_imapd.py
-play/test_imap.py
-play/test-kolab-smtp-access-policy-load.py
-play/test_kolabxml.py
-play/test-login-as.py
-play/test-namespace.py
-play/test-output.py
-play/test_page_control.py
-play/test-parse_ldap_uri.py
-play/test.py
-play/test_sk.ks.c.py
-play/test_socket_client.py
-play/test_socket.py
-play/test_submission.py
-play/test_undelete.py
-play/translit.py
-play/unicode_test.py
-play/unicode-to-ascii.py
-play/wap/domain.info.py
-play/wap/domains.capabilities.py
-play/wap/domains.list.py
-play/wap/form_value.generate_cn.py
-play/wap/form_value.generate_displayname.py
-play/wap/form_value.generate_mail.py
-play/wap/form_value.generate_password.py
-play/wap/form_value.generate_uid.py
-play/wap/form_value.list_options-c.py
-play/wap/form_value.select_options-ou.py
-play/wap/form_value.select_options-preferredlanguage.py
-play/wap/group.add.py
-play/wap/group.info.py
-play/wap/group.members_list.py
-play/wap/groups.list.py
-play/wap/group_types.list.py
-play/wap/role.add.py
-play/wap/role.capabilities.py
-play/wap/role.delete.py
-play/wap/role.find_by_attribute.py
-play/wap/role.info.py
-play/wap/roles.list.py
-play/wap/scu.py
-play/wap/system.capabilities.py
-play/wap/system.select_domain.py
-play/wap/user.add.py
-play/wap/user.delete.py
-play/wap/user.edit.py
-play/wap/user.info.py
-play/wap/users.list.py
-play/wap/user_types.list.py
-play/xmlevent.py
-play/xmlformat.py
-play/xmlfromical.py
-play/zorbadb/test.py
-pykolab/auth/ldap/fds/__init__.py
-pykolab/auth/ldap/msds/__init__.py
-pykolab/auth/ldap/openldap/__init__.py
-pykolab/auth/ldap/rhds/__init__.py
-pykolab/auth/ldap/sunds/__init__.py
-pykolab/auth/sql/__init__.py
-pykolab/cli/cmd_examine_message.py
-pykolab/cli/cmd_list_contacts.py
-pykolab/cli/cmd_list_events.py
-pykolab/cli/cmd_rebalance_mailboxes.py
-pykolab/cli/cmd_role_info.py
-pykolab/cli/cmd_set_quota.py
-pykolab/cli/cmd_summarize_quota_allocation.py
-pykolab/confmgmt/augeas.py
-pykolab/conf/parser.py
+kolabd/.___init__.py
+pykolab/auth/.___init__.py
+pykolab/auth/ldap/._cache.py
+pykolab/auth/ldap/.___init__.py
+pykolab/._base.py
+pykolab/cli/._cmd_create_mailbox.py
+pykolab/cli/._cmd_list_mailbox_metadata.py
+pykolab/cli/._cmd_list_quota.py
+pykolab/cli/._cmd_set_language.py
+pykolab/cli/._cmd_set_mailbox_acl.py
+pykolab/cli/._cmd_set_mail.py
+pykolab/conf/._defaults.py
+pykolab/conf/.___init__.py
+pykolab/._constants.py
 pykolab/constants.py
-pykolab/ical/itip.py
-pykolab/imap/dovecot.py
-pykolab/plugins/roundcube/__init__.py
-pykolab/wap_client.old/__init__.py
-pykolab/xml/task.py
-sievelib-0.5/setup.py
-sievelib-0.5/sievelib/commands.py
-sievelib-0.5/sievelib/digest_md5.py
-sievelib-0.5/sievelib/factory.py
-sievelib-0.5/sievelib/__init__.py
-sievelib-0.5/sievelib/managesieve.py
-sievelib-0.5/sievelib/parser.py
-testaci.py
-test-ask_menu.py
-test-search.py
-test-send-mail-kolab_smtp_access_policy.py
-test-send-mail-kolab_smtp_access_policy-relay.py
-tests/functional/test_kolabd/test_004_many_aliases.py
-test-subscribe-address-to-ml.py
-tests/unit/test-011-base64_encoded_contact.py
-tests/unit/test-012-utf7.py
-test-urllib.py
-test-wallace-loadmsg.py
-test-wallace-resource.py
-test-wallace-send.py
-wallace/future_module_bcc.py
-wallace/future_module_conversations.py
-wallace/future_module_correctsentdate.py
-wallace/future_module_dlp.py
-wallace/future_module_footer.py
-wallace/future_module_freebusy.py
-wallace/future_module_googletranslate.py
-wallace/future_module_statistics.py
-wap_raw.py
+pykolab/._constants.py.in
+pykolab/imap/._cyrus.py
+pykolab/imap/.___init__.py
+pykolab/.___init__.py
+pykolab/itip/.___init__.py
+pykolab/._logger.py
+pykolab/plugins/dynamicquota/.___init__.py
+pykolab/plugins/.___init__.py
+pykolab/plugins/roundcubedb/.___init__.py
+pykolab/setup/.___init__.py
+pykolab/setup/._setup_freebusy.py
+pykolab/setup/._setup_ldap.py
+pykolab/setup/._setup_mta.py
+pykolab/setup/._setup_roundcube.py
+pykolab/._translate.py
+pykolab/._translit.py
+pykolab/._utils.py
+pykolab/wap_client/.___init__.py
+pykolab/xml/._attendee.py
+pykolab/xml/._event.py
+pykolab/xml/.___init__.py
+pykolab/xml/._utils.py
+tests/functional/._purge_users.py
+tests/functional/._resource_func.py
+tests/functional/._synchronize.py
+tests/functional/test_auth/.___init__.py
+tests/functional/test_auth/._test_001_ldap.py
+tests/functional/test_auth/._test_002_sql.py
+tests/functional/test_auth/._test_003_pam.py
+tests/functional/test_auth/._test_004_saslauthd.py
+tests/functional/test_kolabd/._test_001_user_sync.py
+tests/functional/test_wallace/._test_001_user_add.py
+tests/functional/test_wallace/._test_002_footer.py
+tests/functional/test_wallace/._test_005_resource_add.py
+tests/functional/test_wallace/._test_005_resource_invitation.py
+tests/functional/test_wallace/._test_006_resource_performance.py
+tests/functional/test_wallace/._test_007_invitationpolicy.py
+tests/functional/test_wap_client/._test_002_user_add.py
+tests/functional/._user_add.py
+tests/unit/._test-000-imports.py
+tests/unit/._test-002-attendee.py
+tests/unit/._test-003-event.py
+tests/unit/._test-006-ldap_psearch.py
+tests/unit/._test-007-ldap_syncrepl.py
+tests/unit/._test-010-transliterate.py
+tests/unit/._test-011-itip.py
+tests/unit/._test-011-wallace_resources.py
+tests/unit/._test-012-wallace_invitationpolicy.py
+tests/unit/._test-014-conf-and-raw.py
+tests/unit/._test-015-translate.py
+wallace/.___init__.py
+wallace/._module_gpgencrypt.py
+wallace/._module_invitationpolicy.py
+wallace/._module_resources.py
+wallace/._modules.py
diff --git a/po/de.po b/po/de.po
index b49222d..46f34fc 100644
--- a/po/de.po
+++ b/po/de.po
@@ -3,331 +3,362 @@
 # This file is distributed under the same license as the PACKAGE package.
 # 
 # Translators:
-# Christoph Wickert <cwickert at fedoraproject.org>, 2011.
-#   <grote at kolabsys.com>, 2012.
+# Christoph Wickert <christoph.wickert at gmail.com>, 2011
+# Grote <grote at kolabsys.com>, 2012
+# balin <johannes_graumann at web.de>, 2012
+# Jo <jo at caj-augsburg.de>, 2012
+# bitnukl <robert at proemper.net>, 2014
+# Thomas Brüderli <roundcube at gmail.com>, 2014
+# Till Savekoul <till at koul.de>, 2012
 msgid ""
 msgstr ""
 "Project-Id-Version: Kolab Groupware Solution\n"
-"Report-Msgid-Bugs-To: https://isues.kolab.org/\n"
-"POT-Creation-Date: 2012-08-14 12:22+0100\n"
-"PO-Revision-Date: 2012-08-14 11:13+0000\n"
-"Last-Translator: Jeroen van Meeuwen <vanmeeuwen at kolabsys.com>\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-07-10 07:21-0400\n"
+"PO-Revision-Date: 2014-07-22 13:01+0000\n"
+"Last-Translator: Thomas Brüderli <roundcube at gmail.com>\n"
 "Language-Team: German (http://www.transifex.com/projects/p/kolab/language/de/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Language: de\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: ../bin/kolab_smtp_access_policy.py:206
+#: ../bin/kolab_smtp_access_policy.py:209
 #, python-format
 msgid "Adding policy request to instance %s"
 msgstr "Füge Richtlinien-Anfrage zu Instanz %s hinzu"
 
-#: ../bin/kolab_smtp_access_policy.py:446
+#: ../bin/kolab_smtp_access_policy.py:479
 msgid "Unauthorized access not allowed"
 msgstr "Unberechtigter Zugriff nicht erlaubt"
 
-#: ../bin/kolab_smtp_access_policy.py:467
-#: ../bin/kolab_smtp_access_policy.py:657
+#: ../bin/kolab_smtp_access_policy.py:508
+#: ../bin/kolab_smtp_access_policy.py:689
 msgid "Could not find recipient"
 msgstr "Konnte den Empfänger nicht finden"
 
-#: ../bin/kolab_smtp_access_policy.py:486
-#: ../bin/kolab_smtp_access_policy.py:586
+#: ../bin/kolab_smtp_access_policy.py:527
 #, python-format
-msgid "Could not find envelope sender user %s"
-msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden"
+msgid "Could not find envelope sender user %s (511)"
+msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:529
+#: ../bin/kolab_smtp_access_policy.py:570
 #, python-format
 msgid "Obtained authenticated user details for %r: %r"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:608
+#: ../bin/kolab_smtp_access_policy.py:627
+#, python-format
+msgid "Could not find envelope sender user %s"
+msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden"
+
+#: ../bin/kolab_smtp_access_policy.py:649
 #, python-format
 msgid "%s is unauthorized to send on behalf of %s"
 msgstr "Benutzer %s ist nicht berechtigt als Benutzer %s zu senden"
 
-#: ../bin/kolab_smtp_access_policy.py:618
+#: ../bin/kolab_smtp_access_policy.py:659
 #, python-format
 msgid ""
 "User %s attempted to use envelope sender address %s without authorization"
-msgstr ""
+msgstr "Benutzer %s versuchte die Absendeadresse %s ohne Berechtigung zu verwenden"
 
-#: ../bin/kolab_smtp_access_policy.py:681
-#: ../bin/kolab_smtp_access_policy.py:692
+#: ../bin/kolab_smtp_access_policy.py:713
+#: ../bin/kolab_smtp_access_policy.py:724
 #, python-format
 msgid "Found user %s to be a delegate user of %s"
 msgstr "Benutzer %s ist ein delegierter Benutzer von %s"
 
-#: ../bin/kolab_smtp_access_policy.py:716
+#: ../bin/kolab_smtp_access_policy.py:748
 #, python-format
 msgid ""
 "Verifying authenticated sender '%(sender)s' with sasl_username "
 "'%(sasl_username)s' for recipient '%(recipient)s'"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:719
+#: ../bin/kolab_smtp_access_policy.py:751
 #, python-format
 msgid ""
 "Verifying unauthenticated sender '%(sender)s' for recipient '%(recipient)s'"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:735
+#: ../bin/kolab_smtp_access_policy.py:767
 #, python-format
 msgid "Reproducing verify_recipient(%s, %s) from cache"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:753
+#: ../bin/kolab_smtp_access_policy.py:804
 #, python-format
 msgid "Using authentication domain %s instead of %s"
 msgstr "Benutze Authentisierungsdomain %s anstelle von %s"
 
-#: ../bin/kolab_smtp_access_policy.py:763
+#: ../bin/kolab_smtp_access_policy.py:814
 #, python-format
 msgid "Domain %s is a primary domain"
 msgstr "Die Domain %s ist die primäre Domain"
 
-#: ../bin/kolab_smtp_access_policy.py:771
+#: ../bin/kolab_smtp_access_policy.py:822
 #, python-format
 msgid ""
 "Checking the recipient for domain %s that is not ours. This is probably a "
 "configuration error."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:786
+#: ../bin/kolab_smtp_access_policy.py:837
 msgid ""
 "This recipient address is related to multiple object entries and the SMTP "
 "Access Policy can therefore not restrict message flow"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:793
+#: ../bin/kolab_smtp_access_policy.py:854
 #, python-format
 msgid ""
 "Recipient address %r not found. Allowing since the MTA was configured to "
 "accept the recipient."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:820
+#: ../bin/kolab_smtp_access_policy.py:890
 msgid "Invalid recipient"
 msgstr "Ungültiger Empfänger"
 
-#: ../bin/kolab_smtp_access_policy.py:831
+#: ../bin/kolab_smtp_access_policy.py:901
 msgid "Could not find this user, accepting"
 msgstr "Konnte keine Einschränkung für diesen Benutzer finden, akzeptiere Nachricht"
 
-#: ../bin/kolab_smtp_access_policy.py:894
-#: ../bin/kolab_smtp_access_policy.py:945
+#: ../bin/kolab_smtp_access_policy.py:974
+#: ../bin/kolab_smtp_access_policy.py:1050
 #, python-format
 msgid "Sender %s is not allowed to send to recipient %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:933
+#: ../bin/kolab_smtp_access_policy.py:1038
 #, python-format
 msgid "Reproducing verify_sender(%r) from cache"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:960
+#: ../bin/kolab_smtp_access_policy.py:1055
+msgid "Unverifiable sender."
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1060
+msgid "Sender is not using an alias"
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1068
 msgid "Sender uses unauthorized envelope sender address"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:977
+#: ../bin/kolab_smtp_access_policy.py:1085
 msgid "Could not verify sender"
 msgstr "Konnte den Absender nicht verifizieren"
 
-#: ../bin/kolab_smtp_access_policy.py:984
+#: ../bin/kolab_smtp_access_policy.py:1092
 msgid ""
 "Verifying whether sender is allowed to send to recipient using sender policy"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:997
+#: ../bin/kolab_smtp_access_policy.py:1105
 #, python-format
 msgid "Result is %r"
 msgstr "Das Ergebnis ist %r"
 
-#: ../bin/kolab_smtp_access_policy.py:1002
+#: ../bin/kolab_smtp_access_policy.py:1110
 msgid "No recipient policy restrictions exist for this sender"
 msgstr "Es existiert keine Empfängerrichtlinie für diesen Absender"
 
-#: ../bin/kolab_smtp_access_policy.py:1011
+#: ../bin/kolab_smtp_access_policy.py:1119
 msgid "Found a recipient policy to apply for this sender."
 msgstr "Empfänger-Richtlinie für diesen Benutzer gefunden"
 
-#: ../bin/kolab_smtp_access_policy.py:1026
+#: ../bin/kolab_smtp_access_policy.py:1134
 #, python-format
 msgid "Sender %s not allowed to send to recipient %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1056
+#: ../bin/kolab_smtp_access_policy.py:1155
 msgid "Cleaning up the cache"
 msgstr "Aufräumen des Cache"
 
-#: ../bin/kolab_smtp_access_policy.py:1093
+#: ../bin/kolab_smtp_access_policy.py:1177
+msgid ""
+"The 'uri' setting in the kolab_smtp_access_policy section is soon going to "
+"be deprecated in favor of 'cache_uri'"
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1193
 #, python-format
 msgid "Operational Error in caching: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1144
+#: ../bin/kolab_smtp_access_policy.py:1245
 #, python-format
 msgid "Caching the policy result with timestamp %d"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1221
+#: ../bin/kolab_smtp_access_policy.py:1319
 #, python-format
 msgid "Returning action DEFER_IF_PERMIT: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1226
+#: ../bin/kolab_smtp_access_policy.py:1324
 #, python-format
 msgid "Returning action DUNNO: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1231
+#: ../bin/kolab_smtp_access_policy.py:1329
 #, python-format
 msgid "Returning action HOLD: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1236
+#: ../bin/kolab_smtp_access_policy.py:1334
 #, python-format
 msgid "Returning action PERMIT: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1241
+#: ../bin/kolab_smtp_access_policy.py:1459
 #, python-format
 msgid "Returning action REJECT: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1287
+#: ../bin/kolab_smtp_access_policy.py:1505
 msgid "Starting to loop for new request"
 msgstr "Starte Schleife für neue Anfrage"
 
-#: ../bin/kolab_smtp_access_policy.py:1294
+#: ../bin/kolab_smtp_access_policy.py:1512
 msgid "Timeout for policy request reading exceeded"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1300
+#: ../bin/kolab_smtp_access_policy.py:1518
 msgid "End of current request"
 msgstr "Ende der aktuellen Anfrage"
 
-#: ../bin/kolab_smtp_access_policy.py:1304
+#: ../bin/kolab_smtp_access_policy.py:1522
 #, python-format
 msgid "Getting line: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1308
+#: ../bin/kolab_smtp_access_policy.py:1526
 msgid "Returning request"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1334
+#: ../bin/kolab_smtp_access_policy.py:1555
 msgid "Access Policy Options"
 msgstr "Zugriffsrichtlinien-Einstellungen"
 
-#: ../bin/kolab_smtp_access_policy.py:1341
+#: ../bin/kolab_smtp_access_policy.py:1562
 msgid "SMTP Policy request timeout."
 msgstr "Zeitüberschreitung der SMTP Richtlinien-Anfrage"
 
-#: ../bin/kolab_smtp_access_policy.py:1347
+#: ../bin/kolab_smtp_access_policy.py:1568
 msgid "Verify the recipient access policy."
 msgstr "Verifiziere die Empfänger-Zugriffs-Richtlinie."
 
-#: ../bin/kolab_smtp_access_policy.py:1353
+#: ../bin/kolab_smtp_access_policy.py:1574
 msgid "Verify the sender access policy."
 msgstr "Verifiziere die Sender-Zugriffs-Richtlinie."
 
-#: ../bin/kolab_smtp_access_policy.py:1359
+#: ../bin/kolab_smtp_access_policy.py:1580
 msgid "Allow unauthenticated senders."
 msgstr "Erlaube nicht authentisierte Sender."
 
-#: ../bin/kolab_smtp_access_policy.py:1373
+#: ../bin/kolab_smtp_access_policy.py:1594
 #, python-format
 msgid "Got request instance %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1382
+#: ../bin/kolab_smtp_access_policy.py:1603
 #, python-format
 msgid "Request instance %s is in state %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1390
+#: ../bin/kolab_smtp_access_policy.py:1611
 #, python-format
 msgid "Request instance %s is not yet in DATA state"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1402
+#: ../bin/kolab_smtp_access_policy.py:1623
 #, python-format
 msgid "Request instance %s reached DATA state"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1420
+#: ../bin/kolab_smtp_access_policy.py:1643
+#, python-format
+msgid "Unhandled exception caught: %r"
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1647
 msgid "Sender access denied"
 msgstr "Sender Zugriff verweigert"
 
-#: ../bin/kolab_smtp_access_policy.py:1422
+#: ../bin/kolab_smtp_access_policy.py:1649
 msgid "Recipient access denied"
 msgstr "Empfänger Zugriff verweigert"
 
-#: ../bin/kolab_smtp_access_policy.py:1424
+#: ../bin/kolab_smtp_access_policy.py:1651
 msgid "No objections"
 msgstr "Keine Einwände"
 
-#: ../conf.py:37 ../kolab.py:34 ../saslauthd.py:33
+#: ../conf.py:37 ../kolab-cli.py:34 ../saslauthd.py:33
 msgid "Cannot load pykolab/logger.py:"
-msgstr ""
+msgstr "Laden von pykolab/logger.py nicht möglich:"
 
-#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:48
-#: ../wallace/__init__.py:65
+#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:51
+#: ../wallace/__init__.py:85
 msgid "Daemon Options"
 msgstr "Daemon Optionen"
 
-#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:55
-#: ../wallace/__init__.py:72
+#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:58
+#: ../wallace/__init__.py:92
 msgid "Fork to the background."
 msgstr "In den Hintergrund abtauchen"
 
-#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:64
-#: ../wallace/__init__.py:98
+#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:67
+#: ../wallace/__init__.py:118
 msgid "Path to the PID file to use."
 msgstr "Pfad zur PID-Datei"
 
-#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:73
-#: ../wallace/__init__.py:115
+#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76
+#: ../wallace/__init__.py:135
 msgid "Run as user USERNAME"
-msgstr ""
+msgstr "Als Benutzer USERNAME ausführen"
 
-#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:83
-#: ../wallace/__init__.py:89
+#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86
+#: ../wallace/__init__.py:109
 msgid "Run as group GROUPNAME"
-msgstr ""
+msgstr "Als Gruppe GROUPNAME ausführen"
 
-#: ../kolabd/__init__.py:122 ../pykolab/utils.py:151
-#: ../wallace/__init__.py:288
+#: ../kolabd/__init__.py:122 ../pykolab/logger.py:139 ../pykolab/utils.py:234
+#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:329
 #, python-format
 msgid "Group %s does not exist"
-msgstr ""
+msgstr "Gruppe %s exisitert nicht"
 
-#: ../kolabd/__init__.py:131 ../wallace/__init__.py:297
+#: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301
+#: ../wallace/__init__.py:338
 #, python-format
 msgid "Switching real and effective group id to %d"
 msgstr ""
 
-#: ../kolabd/__init__.py:153 ../pykolab/utils.py:175
-#: ../wallace/__init__.py:319
+#: ../kolabd/__init__.py:153 ../pykolab/logger.py:159 ../pykolab/utils.py:258
+#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:360
 #, python-format
 msgid "User %s does not exist"
 msgstr "Benutzer %s existiert nicht"
 
-#: ../kolabd/__init__.py:163 ../wallace/__init__.py:329
+#: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333
+#: ../wallace/__init__.py:370
 #, python-format
 msgid "Switching real and effective user id to %d"
 msgstr ""
 
-#: ../kolabd/__init__.py:172 ../wallace/__init__.py:338
+#: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342
+#: ../wallace/__init__.py:379
 msgid "Could not change real and effective uid and/or gid"
 msgstr ""
 
-#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:122
-#: ../wallace/__init__.py:358
+#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133
+#: ../wallace/__init__.py:399
 msgid "Interrupted by user"
 msgstr "Vom Benutzer unterbrochen"
 
@@ -335,328 +366,492 @@ msgstr "Vom Benutzer unterbrochen"
 msgid "Traceback occurred, please report a "
 msgstr ""
 
-#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:130
-#: ../wallace/__init__.py:367
+#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141
+#: ../wallace/__init__.py:408
 #, python-format
 msgid "Type Error: %s"
 msgstr "Typ-Fehler: %s"
 
-#: ../kolabd/__init__.py:223 ../pykolab/auth/ldap/__init__.py:1591
+#: ../kolabd/__init__.py:230
+msgid "Could not connect to LDAP, is it running?"
+msgstr ""
+
+#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137
 #: ../pykolab/cli/cmd_sync.py:36
 msgid "Listing domains..."
 msgstr "Liste Domains auf..."
 
-#: ../kolabd/__init__.py:256
+#: ../kolabd/__init__.py:244
+msgid "No domains. Not syncing"
+msgstr ""
+
+#: ../kolabd/__init__.py:275
 #, python-format
 msgid "added domains: %r, removed domains: %r"
 msgstr ""
 
+#: ../kolabd/process.py:33
+#, python-format
+msgid "Process created for domain %s"
+msgstr ""
+
+#: ../kolabd/process.py:42
+#, python-format
+msgid "Synchronizing for domain %s"
+msgstr ""
+
+#: ../kolabd/process.py:59
+#, python-format
+msgid ""
+"Error in process %r, terminating:\n"
+"\t%r"
+msgstr ""
+
 #: ../kolabd.py:31 ../setup-kolab.py:36 ../wallace.py:31
 msgid "Cannot load pykolab/constants.py:"
 msgstr "Konnte nicht pykolab/constants.py laden:"
 
-#: ../pykolab/auth/__init__.py:94
+#: ../pykolab/auth/__init__.py:89
 #, python-format
 msgid "Called for domain %r"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:107 ../pykolab/auth/__init__.py:116
+#: ../pykolab/auth/__init__.py:106 ../pykolab/auth/__init__.py:115
 #, python-format
 msgid "Using section %s and domain %s"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:121
+#: ../pykolab/auth/__init__.py:120
 #, python-format
 msgid "Connecting to Authentication backend for domain %s"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:132
+#: ../pykolab/auth/__init__.py:131
 #, python-format
 msgid "Section %s has no option 'auth_mechanism'"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:139
+#: ../pykolab/auth/__init__.py:138
 #, python-format
 msgid "Section %s has auth_mechanism: %r"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:148 ../pykolab/auth/__init__.py:157
+#: ../pykolab/auth/__init__.py:147 ../pykolab/auth/__init__.py:156
 msgid "Starting LDAP..."
 msgstr "Starte LDAP..."
 
-#: ../pykolab/auth/ldap/cache.py:109
+#: ../pykolab/auth/ldap/cache.py:126
 #, python-format
 msgid "Inserting cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/cache.py:122
+#: ../pykolab/auth/ldap/cache.py:147
 #, python-format
 msgid "Updating timestamp for cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/cache.py:129
+#: ../pykolab/auth/ldap/cache.py:155
 #, python-format
 msgid "Updating result_attribute for cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:51
+#: ../pykolab/auth/ldap/__init__.py:52
 msgid "Python LDAP library does not support persistent search"
 msgstr "Die Python LDAP Bibliothek unterstützt keine persistente Suche"
 
-#: ../pykolab/auth/ldap/__init__.py:142
+#: ../pykolab/auth/ldap/__init__.py:143
 #, python-format
 msgid "Attempting to authenticate user %s in realm %s"
 msgstr "Versuche Benutzer %s in Bereich %s zu authentisieren"
 
-#: ../pykolab/auth/ldap/__init__.py:184
+#: ../pykolab/auth/ldap/__init__.py:175 ../pykolab/auth/ldap/__init__.py:226
+#, python-format
+msgid "Authentication cache failed: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:216 ../pykolab/auth/ldap/__init__.py:240
 #, python-format
 msgid "Binding with user_dn %s and password %s"
 msgstr "Binde mit user_dn %s und Passwort %s"
 
-#: ../pykolab/auth/ldap/__init__.py:194
+#: ../pykolab/auth/ldap/__init__.py:231 ../pykolab/auth/ldap/__init__.py:263
 #, python-format
 msgid "Failed to authenticate as user %s"
+msgstr "Autorisation als Benutzer %s gescheitert"
+
+#: ../pykolab/auth/ldap/__init__.py:249
+#, python-format
+msgid "Error occured, there is no such object: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:254
+msgid "Authentication cache failed to clear entry"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:260
+#, python-format
+msgid "Exception occured: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:211
+#: ../pykolab/auth/ldap/__init__.py:280
 msgid "Connecting to LDAP..."
 msgstr "Verbinde zum LDAP..."
 
-#: ../pykolab/auth/ldap/__init__.py:215
+#: ../pykolab/auth/ldap/__init__.py:284
 #, python-format
 msgid "Attempting to use LDAP URI %s"
 msgstr "Versuche LDAP URI %s zu benutzen"
 
-#: ../pykolab/auth/ldap/__init__.py:357
+#: ../pykolab/auth/ldap/__init__.py:371
+#, python-format
+msgid "Entry ID: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:373
+#, python-format
+msgid "Entry DN: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:376
+#, python-format
+msgid ""
+"ldap search: (%r, %r, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:453
 #, python-format
 msgid "Finding recipient with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:425
+#: ../pykolab/auth/ldap/__init__.py:529
 #, python-format
 msgid "Finding resource with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:448
+#: ../pykolab/auth/ldap/__init__.py:560
 #, python-format
 msgid "Using timestamp %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:480
+#: ../pykolab/auth/ldap/__init__.py:595
+msgid "Applying recipient policy disabled through configuration"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:600
 #, python-format
 msgid "Applying recipient policy to %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:497
+#: ../pykolab/auth/ldap/__init__.py:617
 #, python-format
 msgid "Using mail attributes: %r, with primary %r and "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:508
+#: ../pykolab/auth/ldap/__init__.py:628
 #, python-format
 msgid "key %r not in entry"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:510
+#: ../pykolab/auth/ldap/__init__.py:630
 #, python-format
 msgid "key %r is the prim. mail attr."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:512
+#: ../pykolab/auth/ldap/__init__.py:632
 msgid "prim. mail pol. is not empty"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:515
+#: ../pykolab/auth/ldap/__init__.py:635
 #, python-format
 msgid "key %r is the sec. mail attr."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:517
+#: ../pykolab/auth/ldap/__init__.py:637
 msgid "sec. mail pol. is not empty"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:520 ../pykolab/auth/ldap/__init__.py:533
+#: ../pykolab/auth/ldap/__init__.py:641 ../pykolab/auth/ldap/__init__.py:655
 #, python-format
 msgid "Attributes %r are not yet available for entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:574
+#: ../pykolab/auth/ldap/__init__.py:694
 #, python-format
 msgid "No results for mail address %s found"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:585
+#: ../pykolab/auth/ldap/__init__.py:705
 #, python-format
 msgid "1 result for address %s found, verifying"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:595
+#: ../pykolab/auth/ldap/__init__.py:715
 #, python-format
 msgid "Too bad, primary email address %s "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:606 ../pykolab/auth/ldap/__init__.py:695
+#: ../pykolab/auth/ldap/__init__.py:726 ../pykolab/auth/ldap/__init__.py:815
 msgid "Address assigned to us"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:661
+#: ../pykolab/auth/ldap/__init__.py:781
 #, python-format
 msgid "No results for address %s found"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:672
+#: ../pykolab/auth/ldap/__init__.py:792
 #, python-format
 msgid "1 result for address %s found, "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:683
+#: ../pykolab/auth/ldap/__init__.py:803
 msgid "Too bad, secondary email "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:710
+#: ../pykolab/auth/ldap/__init__.py:830
 msgid "Recipient policy composed the following set of secondary "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:716
+#: ../pykolab/auth/ldap/__init__.py:841
 #, python-format
 msgid "Secondary mail addresses that we want is not None: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:727
+#: ../pykolab/auth/ldap/__init__.py:852
 msgid "Avoiding the duplication of the primary mail "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:738
+#: ../pykolab/auth/ldap/__init__.py:863
 #, python-format
 msgid "Entry is getting secondary mail addresses: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:746
+#: ../pykolab/auth/ldap/__init__.py:871
 msgid "Entry did not have any secondary mail "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:772
+#: ../pykolab/auth/ldap/__init__.py:888 ../pykolab/auth/ldap/__init__.py:894
+#, python-format
+msgid "secondary_mail_addresses: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:889 ../pykolab/auth/ldap/__init__.py:895
+#, python-format
+msgid "entry[%s]: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:906
 #, python-format
 msgid "Entry modifications list: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:792
+#: ../pykolab/auth/ldap/__init__.py:934
 #, python-format
 msgid "Setting entry attribute %r to %r for %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:828
+#: ../pykolab/auth/ldap/__init__.py:970
 #, python-format
-msgid "Could not update dn %r"
+msgid ""
+"Could not update dn %r:\n"
+"%r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:841
+#: ../pykolab/auth/ldap/__init__.py:983
 #, python-format
 msgid "Using filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:879
+#: ../pykolab/auth/ldap/__init__.py:998
+#, python-format
+msgid "Synchronization is searching against base DN: %s"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1044
 #, python-format
 msgid "About to consider the user quota for %r (used: %r, "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:945
+#: ../pykolab/auth/ldap/__init__.py:1115
 msgid "Invalid DN, username and/or password."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1229
+#: ../pykolab/auth/ldap/__init__.py:1236 ../pykolab/auth/ldap/__init__.py:1249
+#: ../pykolab/auth/ldap/__init__.py:1614 ../pykolab/auth/ldap/__init__.py:1627
+#, python-format
+msgid "Found a subject %r with access %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1356
+#, python-format
+msgid "Entry %s attribute value: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1364
 #, python-format
-msgid "Current changelog entry %s with %s"
+msgid "imap.user_mailbox_server(%r) result: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1242
+#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853
 #, python-format
 msgid "Result from recipient policy: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1385
+#: ../pykolab/auth/ldap/__init__.py:1908
 #, python-format
 msgid "Kolab user %s does not have a result attribute %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1527
+#: ../pykolab/auth/ldap/__init__.py:2067
 #, python-format
 msgid "Finding domain root dn for domain %s"
-msgstr ""
+msgstr "Suche root dn für die Domain %s"
 
-#: ../pykolab/auth/ldap/__init__.py:1615
+#: ../pykolab/auth/ldap/__init__.py:2164
 msgid "Authentication database DOWN"
 msgstr "Authentisierungsdatenbank UNTEN"
 
-#: ../pykolab/auth/ldap/__init__.py:1699 ../pykolab/auth/ldap/__init__.py:1734
+#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296
 #, python-format
 msgid "Entry type: %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1822
+#: ../pykolab/auth/ldap/__init__.py:2321
+#, python-format
+msgid "Done with _synchronize_callback() for entry %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:2393
 msgid "LDAP Search Result Data Entry:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1838
+#: ../pykolab/auth/ldap/__init__.py:2409
 msgid "Entry Change Notification attributes:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1843
+#: ../pykolab/auth/ldap/__init__.py:2414
 #, python-format
 msgid "Change Type: %r (%r)"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1851
+#: ../pykolab/auth/ldap/__init__.py:2422
 #, python-format
 msgid "Previous DN: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1906
+#: ../pykolab/auth/ldap/__init__.py:2477
 #, python-format
 msgid "Object %s searched no longer exists"
 msgstr "Das gesuchte Objekt %s existiert nicht mehr"
 
-#: ../pykolab/auth/ldap/__init__.py:1916
+#: ../pykolab/auth/ldap/__init__.py:2487
 #, python-format
 msgid "%d results..."
 msgstr "%d Ergebnisse..."
 
-#: ../pykolab/auth/ldap/__init__.py:2014
+#: ../pykolab/auth/ldap/__init__.py:2590
 #, python-format
 msgid "Searching with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2058
+#: ../pykolab/auth/ldap/__init__.py:2642
 #, python-format
 msgid "Checking for support for %s on %s"
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:36 ../pykolab/cli/cmd_create_mailbox.py:36
+#: ../pykolab/auth/ldap/__init__.py:2661
+#, python-format
+msgid "Found support for %s"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:2706
+#, python-format
+msgid "An error occured using %s: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/syncrepl.py:46
+msgid "The name of the persistent, unique attribute "
+msgstr ""
+
+#: ../pykolab/cli/cmd_acl_cleanup.py:34
+msgid "Clean up ACLs that use identifiers that no longer exist"
+msgstr ""
+
+#: ../pykolab/cli/cmd_acl_cleanup.py:56
+#, python-format
+msgid "Deleting ACL %s for subject %s on folder %s"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:42
+msgid "Specify the (new) alias address"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:45
+msgid "Specify the existing recipient address"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:66 ../pykolab/cli/cmd_add_alias.py:70
+#, python-format
+msgid "Domain %r is not a local domain"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:75
+msgid "Primary and secondary domain do not have the same parent domain"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:81
+#, python-format
+msgid "No such recipient %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:87
+#, python-format
+msgid "Recipient for alias %r already exists"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:97
+msgid "Environment is not configured for "
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:105
+#, python-format
+msgid "Recipient %r is not the primary recipient for address %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_domain.py:36
+#: ../pykolab/cli/cmd_count_domain_mailboxes.py:38
+#: ../pykolab/cli/cmd_create_mailbox.py:36
 #: ../pykolab/cli/cmd_export_mailbox.py:33
-#: ../pykolab/cli/cmd_list_mailboxes.py:39
-#: ../pykolab/cli/cmd_list_user_subscriptions.py:35
+#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:38
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:36
+#: ../pykolab/cli/cmd_list_mailboxes.py:40
+#: ../pykolab/cli/cmd_list_mailbox_metadata.py:37
+#: ../pykolab/cli/cmd_list_messages.py:37 ../pykolab/cli/cmd_list_quota.py:36
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:36
+#: ../pykolab/cli/cmd_server_info.py:34
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:38
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:39
+#: ../pykolab/cli/cmd_undelete_mailbox.py:34
 msgid "CLI Options"
 msgstr "Kommandozeilen-Parameter"
 
 #: ../pykolab/cli/cmd_add_domain.py:42
-msgid "Add domain as alias for DOMAIN"
+msgid "Add alias domain."
 msgstr ""
 
 #: ../pykolab/cli/cmd_add_domain.py:47
-msgid "Add a new domain or domain alias."
-msgstr "Füge eine neue Domain oder einen neuen Domain Alias hinzu."
-
-#: ../pykolab/cli/cmd_add_domain.py:55
-msgid "Could not find credentials with sufficient permissions"
+msgid "Add a new domain."
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:80 ../pykolab/wap_client/__init__.py:113
-msgid "Invalid parent domain"
+#: ../pykolab/cli/cmd_add_domain.py:55 ../pykolab/cli/cmd_delete_domain.py:44
+#: ../pykolab/cli/cmd_find_domain.py:44
+msgid "Could not find credentials with sufficient permissions"
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:86
+#: ../pykolab/cli/cmd_add_domain.py:67 ../pykolab/cli/cmd_delete_domain.py:56
+#: ../pykolab/cli/cmd_find_domain.py:56
 msgid "Domain name"
-msgstr ""
+msgstr "Name der Domain"
 
 #: ../pykolab/cli/cmd_add_user_subscription.py:37
 msgid "Subscribe a user to a folder."
@@ -670,7 +865,7 @@ msgid "Folder pattern"
 msgstr ""
 
 #: ../pykolab/cli/cmd_add_user_subscription.py:50
-#: ../pykolab/cli/cmd_list_user_subscriptions.py:56
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:63
 #: ../pykolab/cli/cmd_remove_user_subscription.py:50
 msgid "User ID"
 msgstr "Benutzer ID"
@@ -682,30 +877,39 @@ msgid "Cannot subscribe user to folder %r:"
 msgstr ""
 
 #: ../pykolab/cli/cmd_add_user_subscription.py:73
+#: ../pykolab/cli/cmd_delete_message.py:61
+#: ../pykolab/cli/cmd_list_messages.py:67
 #: ../pykolab/cli/cmd_remove_user_subscription.py:73
 msgid "No such folder"
-msgstr ""
+msgstr "Dieser Ordner ist nicht vorhanden"
 
-#: ../pykolab/cli/cmd_add_user_subscription.py:86
-#, python-format
-msgid "Successfully subscribed user %s to the following folders:"
-msgstr ""
-
-#: ../pykolab/cli/cmd_add_user_subscription.py:92
-#, python-format
-msgid "User %s not subscribed to any folders."
+#: ../pykolab/cli/cmd_count_domain_mailboxes.py:44
+#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:50
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:48
+#: ../pykolab/cli/cmd_list_mailboxes.py:52 ../pykolab/cli/cmd_list_quota.py:42
+#: ../pykolab/cli/cmd_server_info.py:40
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:57
+msgid "List mailboxes on server SERVER only."
 msgstr ""
 
-#: ../pykolab/cli/cmd_create_mailbox.py:41
+#: ../pykolab/cli/cmd_create_mailbox.py:42
 msgid "Set metadata for folder to ANNOTATION=VALUE"
 msgstr ""
 
 #: ../pykolab/cli/cmd_create_mailbox.py:50
-msgid "Invalid argument"
+msgid "Create folder on PARTITION."
 msgstr ""
 
-#: ../pykolab/cli/cmd_create_mailbox.py:58
+#: ../pykolab/cli/cmd_create_mailbox.py:60
+msgid "Invalid argument"
+msgstr "Ungültiges Argument"
+
+#: ../pykolab/cli/cmd_create_mailbox.py:68
 msgid "Invalid argument for metadata"
+msgstr "Ungültiges Argument für die Metadaten"
+
+#: ../pykolab/cli/cmd_delete_domain.py:36
+msgid "Delete a domain."
 msgstr ""
 
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:45
@@ -716,26 +920,48 @@ msgid "ACI Subject"
 msgstr ""
 
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:48
-#: ../pykolab/cli/cmd_list_mailbox_acls.py:41
-#: ../pykolab/cli/cmd_list_mailbox_metadata.py:41
+#: ../pykolab/cli/cmd_list_mailbox_acls.py:43
+#: ../pykolab/cli/cmd_list_mailbox_metadata.py:54
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:54
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:66
+#: ../pykolab/cli/cmd_set_quota.py:46 ../tests/unit/test-015-translate.py:12
+#: ../tests/unit/test-015-translate.py:16
+#: ../tests/unit/test-015-translate.py:18
+#: ../tests/unit/test-015-translate.py:20
 msgid "Folder name"
 msgstr "Ordnername"
 
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:60
-#: ../pykolab/cli/cmd_list_mailbox_acls.py:52
-#: ../pykolab/cli/cmd_list_mailbox_metadata.py:52
+#: ../pykolab/cli/cmd_list_mailbox_acls.py:54
+#: ../pykolab/cli/cmd_list_mailbox_metadata.py:80
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:67
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:94
+#: ../pykolab/cli/cmd_set_quota.py:58
 #, python-format
 msgid "No such folder %r"
-msgstr ""
+msgstr "Kein Ordner %r verfügbar"
 
 #: ../pykolab/cli/cmd_delete_mailbox.py:46
 msgid "No mailbox specified"
 msgstr "Keine Mailbox angegeben"
 
+#: ../pykolab/cli/cmd_delete_mailbox.py:56
+msgid "No such folder(s)"
+msgstr ""
+
+#: ../pykolab/cli/cmd_delete_message.py:36
+msgid "Delete a message from a folder"
+msgstr ""
+
+#: ../pykolab/cli/cmd_delete_message.py:49
+msgid "Specify a UID"
+msgstr ""
+
+#: ../pykolab/cli/cmd_delete_message.py:52
+#: ../pykolab/cli/cmd_list_messages.py:58
+msgid "Specify a folder"
+msgstr ""
+
 #: ../pykolab/cli/cmd_export_mailbox.py:38
 msgid "All folders this user has access to"
 msgstr ""
@@ -743,40 +969,77 @@ msgstr ""
 #: ../pykolab/cli/cmd_export_mailbox.py:108
 #, python-format
 msgid "%s is not a directory"
-msgstr ""
+msgstr "%s ist kein Verzeichnis"
 
 #: ../pykolab/cli/cmd_export_mailbox.py:118
 #, python-format
 msgid "ZIP file at %s.zip"
-msgstr ""
+msgstr "ZIP-Datei unter %s.zip"
 
 #: ../pykolab/cli/cmd_export_mailbox.py:120
 #, python-format
 msgid "No directories found for user %s"
 msgstr ""
 
-#: ../pykolab/cli/cmd_list_mailboxes.py:44
+#: ../pykolab/cli/cmd_find_domain.py:36
+msgid "Find a domain."
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:43
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:41
+#: ../pykolab/cli/cmd_list_mailboxes.py:45
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:41
 msgid "Display raw IMAP UTF-7 folder names"
 msgstr ""
 
-#: ../pykolab/cli/cmd_list_mailboxes.py:75
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:58
+msgid "Domain"
+msgstr "Domäne"
+
+#: ../pykolab/cli/cmd_list_mailboxes.py:87
 #, python-format
 msgid "Appending folder search for %r"
 msgstr ""
 
-#: ../pykolab/cli/cmd_list_user_subscriptions.py:40
+#: ../pykolab/cli/cmd_list_mailbox_metadata.py:44
+msgid "List annotations as user USER"
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_messages.py:43
+msgid "Include messages flagged as \\Deleted"
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_messages.py:47
+msgid "List messages in a folder"
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_quota.py:73 ../pykolab/cli/cmd_list_quota.py:89
+#, python-format
+msgid "The quota for folder %s is set to literally allow 0KB of storage."
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:47
 msgid "List unsubscribed folders"
 msgstr "Liste nicht abonnierte Ordner"
 
-#: ../pykolab/cli/cmd_list_user_subscriptions.py:43
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:50
 msgid "List the folders a user is subscribed to."
 msgstr "Liste die Ordner, die ein Benutzer abonniert hat."
 
-#: ../pykolab/cli/cmd_list_user_subscriptions.py:88
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:98
 #, python-format
 msgid "No unsubscribed folders for user %s"
 msgstr ""
 
+#: ../pykolab/cli/cmd_mailbox_cleanup.py:37
+msgid "Clean up mailboxes that do no longer have an owner."
+msgstr ""
+
+#: ../pykolab/cli/cmd_mailbox_cleanup.py:61
+#, python-format
+msgid "Deleting folder 'user/%s'"
+msgstr ""
+
 #: ../pykolab/cli/cmd_remove_mailaddress.py:49
 msgid "Invalid or unqualified email address."
 msgstr ""
@@ -811,23 +1074,23 @@ msgstr ""
 
 #: ../pykolab/cli/cmd_remove_user_subscription.py:92
 #, python-format
-msgid "User %s not be unsubscribed from any folders."
+msgid "User %s was not unsubscribed from any folders."
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:48
+#: ../pykolab/cli/cmd_rename_mailbox.py:52
 msgid "No target mailbox name specified"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:50
+#: ../pykolab/cli/cmd_rename_mailbox.py:54
 msgid "No source mailbox name specified"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:62
+#: ../pykolab/cli/cmd_rename_mailbox.py:66
 #, python-format
 msgid "Source folder %r does not exist"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:66
+#: ../pykolab/cli/cmd_rename_mailbox.py:70
 #, python-format
 msgid "Target folder %r already exists"
 msgstr ""
@@ -838,17 +1101,75 @@ msgstr ""
 msgid "ACI Permissions"
 msgstr ""
 
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:47
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:51
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:56
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:45
+msgid "Set annotation as user USER"
+msgstr ""
+
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:59
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:63
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:68
 msgid "Metadata value"
 msgstr ""
 
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:50
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:55
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:62
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67
 msgid "Metadata path"
 msgstr ""
 
+#: ../pykolab/cli/cmd_set_quota.py:43 ../pykolab/cli/cmd_set_quota.py:47
+msgid "New quota"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:44
+msgid "Delete mailboxes for recipients that do not appear to exist in LDAP."
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:50
+msgid "Display changes, do not apply them."
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:88
+#, python-format
+msgid "Domains in IMAP not in LDAP: %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:101
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:142
+#, python-format
+msgid ""
+"No recipients for '%s' (would have deleted the mailbox if not for --dry-"
+"run)!"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:106
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:147
+#, python-format
+msgid "Deleting mailbox '%s' because it has no recipients"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:110
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:151
+#, python-format
+msgid "An error occurred removing mailbox %r: %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:112
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:153
+#, python-format
+msgid "Not automatically deleting shared folder '%s'"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:114
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:155
+#, python-format
+msgid "No recipients for '%s' (use --delete to delete)!"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:136
+#, python-format
+msgid "Multiple recipients for '%s'!"
+msgstr ""
+
 #: ../pykolab/cli/cmd_sync.py:41
 #, python-format
 msgid "Found %d domains in %d seconds"
@@ -859,15 +1180,23 @@ msgstr "%d Domains in %d Sekunden gefunden"
 msgid "Running for domain %s"
 msgstr "Starte für Domain %s"
 
-#: ../pykolab/cli/cmd_sync.py:57
+#: ../pykolab/cli/cmd_sync.py:58
 #, python-format
 msgid "Synchronizing users for %s took %d seconds"
 msgstr "Benutzer für %s zu synchronisieren dauerte %d Sekunden"
 
-#: ../pykolab/cli/cmd_undelete_mailbox.py:33
+#: ../pykolab/cli/cmd_undelete_mailbox.py:39
+msgid "Do not actually execute, but state what would have been executed."
+msgstr ""
+
+#: ../pykolab/cli/cmd_undelete_mailbox.py:42
 msgid "Recover mailboxes previously deleted."
 msgstr ""
 
+#: ../pykolab/cli/cmd_user_info.py:39
+msgid "Email address"
+msgstr "E-Mail-Adresse"
+
 #. This is a nested command
 #. This is a nested component
 #: ../pykolab/cli/commands.py:98 ../pykolab/setup/components.py:90
@@ -875,25 +1204,102 @@ msgstr ""
 msgid "Command Group: %s"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:109 ../pykolab/cli/commands.py:114
+#: ../pykolab/cli/commands.py:113 ../pykolab/cli/commands.py:118
 msgid "No such command."
 msgstr "Dieses Kommando existiert nicht."
 
-#: ../pykolab/cli/commands.py:165 ../pykolab/setup/components.py:231
+#: ../pykolab/cli/commands.py:168 ../pykolab/setup/components.py:231
 #, python-format
 msgid "Command '%s' already registered"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:190 ../pykolab/setup/components.py:257
-#: ../wallace/modules.py:338
+#: ../pykolab/cli/commands.py:193 ../pykolab/setup/components.py:257
+#: ../wallace/modules.py:369
 #, python-format
 msgid "Alias for %s"
 msgstr "Alias für %s"
 
-#: ../pykolab/cli/commands.py:198 ../pykolab/setup/components.py:265
+#: ../pykolab/cli/commands.py:201 ../pykolab/setup/components.py:265
 msgid "Not yet implemented"
 msgstr "Diese Funktion ist noch nicht implementiert"
 
+#: ../pykolab/cli/sieve/cmd_list.py:43 ../pykolab/cli/sieve/cmd_put.py:42
+#: ../pykolab/cli/sieve/cmd_refresh.py:44 ../pykolab/cli/sieve/cmd_test.py:43
+msgid "Email Address"
+msgstr "E-mail-Adresse"
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:99
+#: ../pykolab/plugins/sievemgmt/__init__.py:111
+#, python-format
+msgid "Found the following scripts for user %s: %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:100
+#: ../pykolab/plugins/sievemgmt/__init__.py:112
+#, python-format
+msgid "And the following script is active for user %s: %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:178
+#: ../pykolab/plugins/sievemgmt/__init__.py:190
+#, python-format
+msgid ""
+"Delivery to folder active, but no folder name attribute available for user "
+"%r"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:181
+#: ../pykolab/plugins/sievemgmt/__init__.py:193
+msgid "Delivery to folder active, but no folder name attribute configured"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:359
+#, python-format
+msgid "MANAGEMENT script for user %s contents: %r"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:364
+#: ../pykolab/plugins/sievemgmt/__init__.py:374
+#, python-format
+msgid "Uploading script MANAGEMENT failed for user %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:366
+#: ../pykolab/plugins/sievemgmt/__init__.py:376
+#, python-format
+msgid "Uploading script MANAGEMENT for user %s succeeded"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:377
+#: ../pykolab/plugins/sievemgmt/__init__.py:387
+#, python-format
+msgid "Including script %s in USER (for user %s)"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:386
+#: ../pykolab/plugins/sievemgmt/__init__.py:396
+#, python-format
+msgid "Uploading script USER failed for user %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:388
+#: ../pykolab/plugins/sievemgmt/__init__.py:398
+#, python-format
+msgid "Uploading script USER for user %s succeeded"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:416
+#: ../pykolab/plugins/sievemgmt/__init__.py:426
+#, python-format
+msgid "Uploading script MASTER failed for user %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:418
+#: ../pykolab/plugins/sievemgmt/__init__.py:428
+#, python-format
+msgid "Uploading script MASTER for user %s succeeded"
+msgstr ""
+
 #: ../pykolab/cli/telemetry/cmd_examine_command_issue.py:40
 msgid "Unspecified command issue identifier"
 msgstr ""
@@ -958,27 +1364,27 @@ msgstr ""
 #: ../pykolab/conf/__init__.py:87
 #, python-format
 msgid "Setting %s to %r (from defaults)"
-msgstr ""
+msgstr "Setze %s auf %r (aus den Standardeinstellungen)"
 
 #: ../pykolab/conf/__init__.py:106
 #, python-format
 msgid "Setting %s to %r (from CLI, verified)"
-msgstr ""
+msgstr "Setze %s auf %r (von der Kommandozeile, überprüft)"
 
 #: ../pykolab/conf/__init__.py:109
 #, python-format
 msgid "Setting %s to %r (from CLI, not checked)"
-msgstr ""
+msgstr "Setze %s auf %r (von der Kommandozeile, nicht überprüft)"
 
 #: ../pykolab/conf/__init__.py:150 ../pykolab/conf/__init__.py:207
 #, python-format
 msgid "Setting %s_%s to '****' (from configuration file)"
-msgstr ""
+msgstr "Setze %s_%s auf '****' (aus Konfigurationsdatei)"
 
 #: ../pykolab/conf/__init__.py:152 ../pykolab/conf/__init__.py:209
 #, python-format
 msgid "Setting %s_%s to %r (from configuration file)"
-msgstr ""
+msgstr "Setze %s_%s auf %r (aus Konfigurationsdatei)"
 
 #: ../pykolab/conf/__init__.py:162
 msgid "Setting options from configuration file"
@@ -1039,76 +1445,76 @@ msgstr "Ja auf alle Fragen."
 msgid "No command supplied"
 msgstr "Kein Befehl angegeben"
 
-#: ../pykolab/conf/__init__.py:411
+#: ../pykolab/conf/__init__.py:416
 msgid "Insufficient options. Need section, key and value -in that order."
 msgstr "Unzureichende Optionen. Brauche Sektion, Schlüssel und Wert in dieser Reihenfolge."
 
-#: ../pykolab/conf/__init__.py:414
+#: ../pykolab/conf/__init__.py:419
 #, python-format
 msgid "No section '%s' exists."
 msgstr "Es existiert keine Sektion '%s'."
 
-#: ../pykolab/conf/__init__.py:445
+#: ../pykolab/conf/__init__.py:461
 #, python-format
 msgid "Setting %s to %r (from the default values for CLI options)"
-msgstr ""
+msgstr "Setze %s auf %r (aus den Standardwerten für Kommandozeilenoptionen)"
 
-#: ../pykolab/conf/__init__.py:514
+#: ../pykolab/conf/__init__.py:534
 #, python-format
 msgid "Could not execute configuration function: %s"
-msgstr ""
+msgstr "Konnte die Konfigurationsfunktion nicht ausführen: %s"
 
-#: ../pykolab/conf/__init__.py:522
+#: ../pykolab/conf/__init__.py:542
 #, python-format
 msgid "Option %s/%s does not exist in config file %s, pulling from defaults"
-msgstr ""
+msgstr "Die Option %s/%s existiert in der Konfigurationsdatei %s nicht, sie wird aus den Standardeinstellungen geholt"
 
-#: ../pykolab/conf/__init__.py:530 ../pykolab/conf/__init__.py:533
+#: ../pykolab/conf/__init__.py:550 ../pykolab/conf/__init__.py:553
 msgid "Option does not exist in defaults."
 msgstr "Diese Option hat keinen Standardwert."
 
-#: ../pykolab/conf/__init__.py:543
+#: ../pykolab/conf/__init__.py:563
 #, python-format
 msgid "Configuration file %s not readable."
 msgstr "Konfigurationsdatei %s ist nicht lesbar."
 
-#: ../pykolab/conf/__init__.py:546
+#: ../pykolab/conf/__init__.py:566
 #, python-format
 msgid "Configuration file %s does not exist."
 msgstr "Konfigurationsdatei %s existiert nicht."
 
-#: ../pykolab/conf/__init__.py:551
+#: ../pykolab/conf/__init__.py:571
 msgid ""
 "WARNING: A negative debug level value does not make this program be any more"
 " silent."
 msgstr "Warnung: Eine negative Fehlerprotokollierungszahl macht dieses Programm nicht noch stiller."
 
-#: ../pykolab/conf/__init__.py:557
+#: ../pykolab/conf/__init__.py:577
 msgid "This program has 9 levels of verbosity. Using the maximum of 9."
 msgstr "Dieses Programm hat 9 Ebenen der Detailliertheit. Benutze das Maximum 9."
 
-#: ../pykolab/conf/__init__.py:565 ../pykolab/conf/__init__.py:571
+#: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591
 msgid "Cannot start SASL authentication daemon"
 msgstr "Konnte SASL Authentisierungsdaemon nicht starten"
 
-#: ../pykolab/conf/__init__.py:582
+#: ../pykolab/conf/__init__.py:602
 msgid "No imaplib library found."
-msgstr ""
+msgstr "Keine imaplib-Bibliothek gefunden."
 
-#: ../pykolab/conf/__init__.py:592
+#: ../pykolab/conf/__init__.py:612
 msgid "No LMTP class found in the smtplib library."
-msgstr ""
+msgstr "Keine Klasse namens LMTP in der smtplib-Bibliothek gefunden."
 
-#: ../pykolab/conf/__init__.py:602
+#: ../pykolab/conf/__init__.py:622
 msgid "No SMTP class found in the smtplib library."
-msgstr ""
+msgstr "Keine Klasse namens SMTP in der smtplib-Bibliothek gefunden."
 
-#: ../pykolab/conf/__init__.py:616
+#: ../pykolab/conf/__init__.py:636
 #, python-format
 msgid "Found you specified a specific set of items to test: %s"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:624
+#: ../pykolab/conf/__init__.py:644
 #, python-format
 msgid "Selectively selecting: %s"
 msgstr ""
@@ -1137,263 +1543,383 @@ msgstr "389 Verzeichnisserver oder Red Hat Verzeichnisserver"
 msgid "OpenLDAP or compatible"
 msgstr "OpenLDAP oder kompatibel"
 
-#: ../pykolab/imap/cyrus.py:79
+#: ../pykolab/imap/cyrus.py:80
 #, python-format
 msgid "Could not connect to Cyrus IMAP server %r"
-msgstr ""
+msgstr "Verbindung zum Cyrus IMAP-Server %r nicht möglich"
 
-#: ../pykolab/imap/cyrus.py:134
+#: ../pykolab/imap/cyrus.py:137
 #, python-format
 msgid "Continuing with separator: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:139
+#: ../pykolab/imap/cyrus.py:142
 msgid "Detected we are running in a Murder topology"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:143
+#: ../pykolab/imap/cyrus.py:146
 msgid "This system is not part of a murder topology"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:164
+#: ../pykolab/imap/cyrus.py:167
 #, python-format
 msgid "Checking actual backend server for folder %s through annotations"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:179
+#: ../pykolab/imap/cyrus.py:172
+msgid "Possibly reproducing the find "
+msgstr ""
+
+#: ../pykolab/imap/cyrus.py:195
 #, python-format
 msgid "Could not get the annotations after %s tries."
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:183
+#: ../pykolab/imap/cyrus.py:199
 #, python-format
 msgid "No annotations for %s: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:190
+#: ../pykolab/imap/cyrus.py:206
 #, python-format
 msgid "Server for INBOX folder %s is %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:202
+#: ../pykolab/imap/cyrus.py:226
 #, python-format
 msgid "Setting quota for folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:206
+#: ../pykolab/imap/cyrus.py:230
 #, python-format
 msgid "Could not set quota for mailfolder %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:215
+#: ../pykolab/imap/cyrus.py:239
 #, python-format
 msgid "Moving INBOX folder %s to %s"
-msgstr ""
+msgstr "Verschiebe Eingangsordner %s nach %s"
 
-#: ../pykolab/imap/cyrus.py:227
+#: ../pykolab/imap/cyrus.py:254
 #, python-format
 msgid "Setting annotation %s on folder %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:234
+#: ../pykolab/imap/cyrus.py:259
 #, python-format
 msgid "Could not set annotation %r on mail folder %r: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:238
+#: ../pykolab/imap/cyrus.py:263
 #, python-format
 msgid "Transferring folder %s from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:298
+#: ../pykolab/imap/cyrus.py:323
 #, python-format
 msgid "Undeleting %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:45
+#: ../pykolab/imap/cyrus.py:334
+#, python-format
+msgid "Would have transfered %s from %s to %s"
+msgstr ""
+
+#: ../pykolab/imap/cyrus.py:336
+#, python-format
+msgid "Would have renamed %s to %s"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:46
 #, python-format
 msgid "Cleaning up ACL entries for %s across all folders"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:60
+#: ../pykolab/imap/__init__.py:61
 #, python-format
 msgid "Cleaning up ACL entries referring to identifier %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:69
+#: ../pykolab/imap/__init__.py:70
 #, python-format
 msgid "Iterating over %d folders"
 msgstr ""
 
 #. Set the ACL to '' (effectively deleting the ACL entry)
-#: ../pykolab/imap/__init__.py:82
+#: ../pykolab/imap/__init__.py:83
 #, python-format
 msgid "Removing acl %r for subject %r from folder %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:143
+#: ../pykolab/imap/__init__.py:145
+msgid "No administrator password is available."
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:153
 #, python-format
 msgid "Logging on to Cyrus IMAP server %s"
-msgstr ""
+msgstr "Anmeldung am Cyrus IMAP Server %s"
 
-#: ../pykolab/imap/__init__.py:152
+#: ../pykolab/imap/__init__.py:162
 #, python-format
 msgid "Logging on to Dovecot IMAP server %s"
-msgstr ""
+msgstr "Anmeldung am Dovecot IMAP Server %s"
 
-#: ../pykolab/imap/__init__.py:161
+#: ../pykolab/imap/__init__.py:171
 #, python-format
 msgid "Logging on to generic IMAP server %s"
-msgstr ""
+msgstr "Anmeldung am generischen IMAP Server %s"
 
-#: ../pykolab/imap/__init__.py:179
+#: ../pykolab/imap/__init__.py:189
 #, python-format
 msgid "Reusing existing IMAP server connection to %s"
 msgstr "Benutze Verbindung zum IMAP Server %s wieder"
 
-#: ../pykolab/imap/__init__.py:181
+#: ../pykolab/imap/__init__.py:191
 #, python-format
 msgid "Reconnecting to IMAP server %s"
 msgstr "Verbinde nochmal zum IMAP Server %s"
 
-#: ../pykolab/imap/__init__.py:197
+#: ../pykolab/imap/__init__.py:208
 msgid "Called imap.disconnect() on a server that we had no connection to."
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:212
+#: ../pykolab/imap/__init__.py:222 ../pykolab/imap/__init__.py:234
 #, python-format
-msgid "%r has no attribute %s"
-msgstr "%r hat kein Attribut %s"
+msgid "Could not create folder %r"
+msgstr ""
 
-#: ../pykolab/imap/__init__.py:285
-msgid "Private annotations need to be set using the appropriate user account."
+#: ../pykolab/imap/__init__.py:223
+#, python-format
+msgid " on server %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:318 ../pykolab/imap/__init__.py:353
+#: ../pykolab/imap/__init__.py:244 ../pykolab/imap/__init__.py:246
+#, python-format
+msgid "%r has no attribute %s"
+msgstr "%r hat kein Attribut %s"
+
+#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428
 #, python-format
 msgid "Creating new shared folder %s"
 msgstr "Erzeuge einen neuen geteilten Ordner %s"
 
-#: ../pykolab/imap/__init__.py:375
+#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675
+#, python-format
+msgid "Downcasing mailbox name %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:457
 #, python-format
 msgid "Creating new mailbox for user %s"
 msgstr "Erzeuge eine neue Mailbox für Benutzer %s"
 
-#: ../pykolab/imap/__init__.py:404
+#: ../pykolab/imap/__init__.py:470
+msgid "Waiting for the Cyrus IMAP Murder to settle..."
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:516
 #, python-format
 msgid "Creating additional folders for user %s"
 msgstr "Erzeuge weitere Order für Benutzer %s"
 
-#: ../pykolab/imap/__init__.py:428
+#: ../pykolab/imap/__init__.py:535
+#, python-format
+msgid "Waiting for the Cyrus murder to settle... %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:547
+#, python-format
+msgid "Correcting additional folder name from %r to %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:553
 #, python-format
 msgid "Mailbox already exists: %s"
 msgstr "Mailbox existiert bereits: %s"
 
-#: ../pykolab/imap/__init__.py:471
+#: ../pykolab/imap/__init__.py:593
 msgid "Subscribing user to the additional folders"
 msgstr "Abonniere weitere Ordner für den Benutzer"
 
-#: ../pykolab/imap/__init__.py:531 ../pykolab/imap/__init__.py:605
+#: ../pykolab/imap/__init__.py:607
+msgid "Using the following tests for folder subscriptions:"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:609
+#, python-format
+msgid "    %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:612
+#, python-format
+msgid "Folder %s"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:624
+#, python-format
+msgid "Subscribing %s to folder %s"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:628
+#, python-format
+msgid "Subscribing %s to folder %s failed: %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:658
+#, python-format
+msgid "Could not rename %s to reside on partition %s"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:691
+#, python-format
+msgid "INBOX folder to rename (%s) does not exist"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770
 #, python-format
 msgid "Renaming INBOX from %s to %s"
 msgstr "Benenne INBOX Ordner von %s in %s um"
 
-#: ../pykolab/imap/__init__.py:535
+#: ../pykolab/imap/__init__.py:698
 #, python-format
 msgid "Could not rename INBOX folder %s to %s"
 msgstr "Konnte INBOX Ordner nicht von %s in %s umbenennen"
 
-#: ../pykolab/imap/__init__.py:537 ../pykolab/imap/__init__.py:609
+#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774
 #, python-format
 msgid ""
 "Moving INBOX folder %s won't succeed as target folder %s already exists"
+msgstr "Der INBOX-Ordner %s kann nicht verschoben werden, weil der Zielordner %s bereits existiert"
+
+#: ../pykolab/imap/__init__.py:704
+#, python-format
+msgid "Server for mailbox %r is %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:547
+#: ../pykolab/imap/__init__.py:712
 #, python-format
 msgid "Looking for folder '%s', we found folders: %r"
-msgstr ""
+msgstr "Auf der Suche nach dem Ordner '%s' haben wir diese Ordner gefunden: %r"
 
-#: ../pykolab/imap/__init__.py:570
+#: ../pykolab/imap/__init__.py:735
 #, python-format
 msgid "Setting ACL rights %s for subject %s on folder "
 msgstr "Richte ACL Rechte %s für Subjekt %s des Ordners ein"
 
-#: ../pykolab/imap/__init__.py:581
+#: ../pykolab/imap/__init__.py:746
 #, python-format
 msgid "Removing ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:602
+#: ../pykolab/imap/__init__.py:767
 #, python-format
 msgid "Found old INBOX folder %s"
-msgstr ""
+msgstr "Alter INBOX-Ordner %s gefunden"
 
-#: ../pykolab/imap/__init__.py:611
+#: ../pykolab/imap/__init__.py:776
 #, python-format
 msgid "Did not find old folder user/%s to rename"
 msgstr "Konnte alten Ordner user/%s nicht zum umbenennen finden."
 
-#: ../pykolab/imap/__init__.py:613
+#: ../pykolab/imap/__init__.py:778
 msgid "Value for user is not a dictionary"
 msgstr "Der Wert für user ist kein dictionary"
 
 #. TODO: Go in fact correct the quota.
-#: ../pykolab/imap/__init__.py:673
+#: ../pykolab/imap/__init__.py:846
 #, python-format
 msgid "Cannot get current IMAP quota for folder %s"
 msgstr "Kann aktuelles IMAP Kontingent für den Ordner %s nicht bekommen"
 
-#: ../pykolab/imap/__init__.py:686
+#: ../pykolab/imap/__init__.py:859
 #, python-format
 msgid "Quota for %s currently is %s"
 msgstr "Kontingent für %s ist aktuell %s"
 
-#: ../pykolab/imap/__init__.py:692
+#: ../pykolab/imap/__init__.py:865
 #, python-format
 msgid "Adjusting authentication database quota for folder %s to %d"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:697
+#: ../pykolab/imap/__init__.py:870
 #, python-format
 msgid "Correcting quota for %s to %s (currently %s)"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:774
+#: ../pykolab/imap/__init__.py:947
 #, python-format
 msgid "Checking folder: %s"
 msgstr "Überprüfe Ordner: %s"
 
-#: ../pykolab/imap/__init__.py:779
+#: ../pykolab/imap/__init__.py:952
 #, python-format
 msgid "Folder has no corresponding user (1): %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:782
+#: ../pykolab/imap/__init__.py:955
 #, python-format
 msgid "Folder has no corresponding user (2): %s"
 msgstr ""
 
 #. We got user identifier only
-#: ../pykolab/imap/__init__.py:797
+#: ../pykolab/imap/__init__.py:970
 msgid "Please don't give us just a user identifier"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:800
+#: ../pykolab/imap/__init__.py:973
 #, python-format
 msgid "Deleting folder %s"
-msgstr ""
+msgstr "Lösche Verzeichnis %s"
 
 #: ../pykolab/__init__.py:50
 msgid "Returning thread local configuration"
 msgstr ""
 
-#: ../pykolab/logger.py:105
+#: ../pykolab/itip/__init__.py:43
+#, python-format
+msgid "Method %r not really interesting for us."
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:49
 #, python-format
-msgid "Could not change the ownership of log file %s"
+msgid "Raw iTip payload: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:59
+msgid "Could not read iTip from message."
 msgstr ""
 
-#: ../pykolab/logger.py:121
+#: ../pykolab/itip/__init__.py:67
+#, python-format
+msgid "Duplicate iTip object: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:90
+msgid "iTip event without a start"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:132
+msgid "Message is not an iTip message (non-multipart message)"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:225
+#, python-format
+msgid "Failed to compose iTip reply message: %r"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936
+#: ../wallace/module_resources.py:964
+#, python-format
+msgid "SMTP sendmail error: %r"
+msgstr ""
+
+#: ../pykolab/logger.py:173 ../pykolab/logger.py:179
+#, python-format
+msgid "Could not change permissions on %s: %r"
+msgstr ""
+
+#: ../pykolab/logger.py:196
 #, python-format
 msgid "Cannot log to file %s: %s"
 msgstr ""
@@ -1425,12 +1951,12 @@ msgstr ""
 #: ../pykolab/plugins/__init__.py:74
 #, python-format
 msgid "RuntimeError for plugin %s: %s"
-msgstr ""
+msgstr "Laufzeitfehler von Plugin %s: %s"
 
 #: ../pykolab/plugins/__init__.py:78
 #, python-format
 msgid "Plugin %s failed to load (%s: %s)"
-msgstr ""
+msgstr "Plugin %s konnte nicht geladen werden (%s: %s)"
 
 #: ../pykolab/plugins/__init__.py:116 ../pykolab/plugins/__init__.py:118
 #, python-format
@@ -1470,7 +1996,7 @@ msgstr ""
 #: ../pykolab/plugins/__init__.py:187
 #, python-format
 msgid "Cannot check options for plugin %s: %s"
-msgstr ""
+msgstr "Kein Zugriff auf Einstellungen des Plugins %s: %s"
 
 #: ../pykolab/plugins/__init__.py:189
 #, python-format
@@ -1491,16 +2017,51 @@ msgstr ""
 msgid "Attribute substitution for 'mail' failed in Recipient Policy"
 msgstr ""
 
-#: ../pykolab/plugins/recipientpolicy/__init__.py:115
+#: ../pykolab/plugins/recipientpolicy/__init__.py:116
 msgid "Could not parse the alternative mail routines"
 msgstr ""
 
+#: ../pykolab/plugins/recipientpolicy/__init__.py:120
+#, python-format
+msgid "Alternative mail routines: %r"
+msgstr ""
+
 #: ../pykolab/plugins/recipientpolicy/__init__.py:127
-#: ../pykolab/plugins/recipientpolicy/__init__.py:137
+#, python-format
+msgid ""
+"An error occurred in composing the secondary mail attribute for entry %r"
+msgstr ""
+
+#: ../pykolab/plugins/recipientpolicy/__init__.py:138
+#: ../pykolab/plugins/recipientpolicy/__init__.py:153
+#, python-format
+msgid "Appending additional mail address: %s"
+msgstr ""
+
+#: ../pykolab/plugins/recipientpolicy/__init__.py:142
+#, python-format
+msgid "Policy for secondary email address failed: %r"
+msgstr ""
+
+#: ../pykolab/plugins/recipientpolicy/__init__.py:157
 msgid ""
 "Attribute substitution for 'alternative_mail' failed in Recipient Policy"
 msgstr ""
 
+#: ../pykolab/plugins/roundcubedb/__init__.py:48
+#, python-format
+msgid "user_delete: %r"
+msgstr ""
+
+#: ../pykolab/plugins/roundcubedb/__init__.py:55
+#: ../pykolab/setup/setup_roundcube.py:160
+msgid "Roundcube installation path not found."
+msgstr ""
+
+#: ../pykolab/plugins/sievemgmt/__init__.py:51
+msgid "Wrong number of arguments for sieve management plugin"
+msgstr ""
+
 #: ../pykolab/setup/components.py:58
 msgid "Display this help."
 msgstr "Zeige diese Hilfe."
@@ -1521,47 +2082,27 @@ msgstr ""
 msgid "Free/Busy is not installed on this system"
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:55
-msgid ""
-"\n"
-"                        Please supply the MySQL password for the 'roundcube'\n"
-"                        user. You have supplied this password earlier, and it is\n"
-"                        available from the database URI setting in\n"
-"                        /etc/roundcubemail/db.inc.php.\n"
-"                    "
-msgstr ""
-
-#: ../pykolab/setup/setup_freebusy.py:64
-#: ../pykolab/setup/setup_roundcube.py:56
-msgid "MySQL roundcube password"
-msgstr "MySQL roundcube Passwort"
+#: ../pykolab/setup/setup_imap.py:45
+msgid "Setup IMAP."
+msgstr "Richte IMAP ein."
 
-#: ../pykolab/setup/setup_freebusy.py:92
-#: ../pykolab/setup/setup_roundcube.py:116 ../pykolab/setup/setup_zpush.py:71
-#, python-format
-msgid "Using template file %r"
+#: ../pykolab/setup/setup_imap.py:89
+msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:99
-#: ../pykolab/setup/setup_roundcube.py:123 ../pykolab/setup/setup_zpush.py:78
-#, python-format
-msgid "Successfully compiled template %r, writing out to %r"
+#: ../pykolab/setup/setup_imap.py:114
+msgid "Could not write out Cyrus IMAP configuration file /etc/cyrus.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:119 ../pykolab/setup/setup_imap.py:143
-#: ../pykolab/setup/setup_ldap.py:288 ../pykolab/setup/setup_ldap.py:521
-#: ../pykolab/setup/setup_mta.py:309 ../pykolab/setup/setup_mysql.py:49
-#: ../pykolab/setup/setup_roundcube.py:191
-#: ../pykolab/setup/setup_syncroton.py:66 ../pykolab/setup/setup_zpush.py:98
-msgid "Could not start and configure to start on boot, the "
+#: ../pykolab/setup/setup_imap.py:158
+msgid "Could not start the cyrus-imapd and kolab-saslauthd services."
 msgstr ""
 
-#: ../pykolab/setup/setup_imap.py:44
-msgid "Setup IMAP."
-msgstr "Richte IMAP ein."
-
-#: ../pykolab/setup/setup_imap.py:88 ../pykolab/setup/setup_imap.py:113
-msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf"
+#: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:81
+#: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:455
+#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:237
+#: ../pykolab/setup/setup_syncroton.py:102
+msgid "Could not configure to start on boot, the "
 msgstr ""
 
 #: ../pykolab/setup/setup_kolabd.py:43
@@ -1577,23 +2118,62 @@ msgid ""
 "                        "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:43
+#: ../pykolab/setup/setup_kolabd.py:72
+msgid "Could not start the kolab server service."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:45
 msgid "LDAP Options"
 msgstr "LDAP Optionen"
 
-#: ../pykolab/setup/setup_ldap.py:50
+#: ../pykolab/setup/setup_ldap.py:52
 msgid "Specify FQDN (overriding defaults)."
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:58
+#: ../pykolab/setup/setup_ldap.py:60
 msgid "Allow anonymous binds (default: no)."
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:62
+#: ../pykolab/setup/setup_ldap.py:68
+msgid "Skip setting up the LDAP server."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:76
+msgid "Setup configuration for OpenLDAP compatibility."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:84
+msgid "Setup configuration for Active Directory compatibility."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:88
 msgid "Setup LDAP."
 msgstr "LDAP Einrichten"
 
-#: ../pykolab/setup/setup_ldap.py:74
+#: ../pykolab/setup/setup_ldap.py:97
+msgid "Skipping setup of LDAP, as specified"
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:126
+msgid ""
+"\n"
+"                        You can not configure Kolab to run against OpenLDAP\n"
+"                        and Active Directory simultaneously.\n"
+"                    "
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:139
+msgid ""
+"\n"
+"                                It seems 389 Directory Server has an existing\n"
+"                                instance configured. This setup script does not\n"
+"                                intend to destroy or overwrite your data. Please\n"
+"                                make sure /etc/dirsrv/ and /var/lib/dirsrv/ are\n"
+"                                clean so that this setup does not have to worry.\n"
+"                            "
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:154
 msgid ""
 "\n"
 "                        Please supply a password for the LDAP administrator user\n"
@@ -1602,11 +2182,11 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:82
+#: ../pykolab/setup/setup_ldap.py:162
 msgid "Administrator password"
 msgstr "Administrator Passwort"
 
-#: ../pykolab/setup/setup_ldap.py:89
+#: ../pykolab/setup/setup_ldap.py:169
 msgid ""
 "\n"
 "                        Please supply a password for the LDAP Directory Manager\n"
@@ -1616,11 +2196,11 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:98
+#: ../pykolab/setup/setup_ldap.py:178
 msgid "Directory Manager password"
 msgstr "Verzeichnismanager Passwort"
 
-#: ../pykolab/setup/setup_ldap.py:105
+#: ../pykolab/setup/setup_ldap.py:185
 msgid ""
 "\n"
 "                        Please choose the system user and group the service\n"
@@ -1629,15 +2209,15 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:112
+#: ../pykolab/setup/setup_ldap.py:195 ../pykolab/setup/setup_ldap.py:198
 msgid "User"
 msgstr "Benutzer"
 
-#: ../pykolab/setup/setup_ldap.py:113
+#: ../pykolab/setup/setup_ldap.py:196 ../pykolab/setup/setup_ldap.py:199
 msgid "Group"
 msgstr "Gruppe"
 
-#: ../pykolab/setup/setup_ldap.py:143
+#: ../pykolab/setup/setup_ldap.py:234
 msgid ""
 "\n"
 "                        This setup procedure plans to set up Kolab Groupware for\n"
@@ -1648,18 +2228,18 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:157
+#: ../pykolab/setup/setup_ldap.py:248
 msgid "Domain name to use"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:162 ../pykolab/setup/setup_ldap.py:187
+#: ../pykolab/setup/setup_ldap.py:253 ../pykolab/setup/setup_ldap.py:278
 msgid ""
 "\n"
 "                                    Invalid input. Please try again.\n"
 "                                "
-msgstr ""
+msgstr "\n                                    Ungültige Eingabe. Bitte nochmals versuchen.\n                                "
 
-#: ../pykolab/setup/setup_ldap.py:171
+#: ../pykolab/setup/setup_ldap.py:262
 msgid ""
 "\n"
 "                        The standard root dn we composed for you follows. Please\n"
@@ -1667,11 +2247,15 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:182
+#: ../pykolab/setup/setup_ldap.py:273
 msgid "Root DN to use"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:234
+#: ../pykolab/setup/setup_ldap.py:325
+msgid "No directory server setup tool available."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:337
 msgid ""
 "\n"
 "                    Setup is now going to set up the 389 Directory Server. This\n"
@@ -1680,24 +2264,42 @@ msgid ""
 "                "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:241
+#: ../pykolab/setup/setup_ldap.py:344
 msgid "Setting up 389 Directory Server"
 msgstr "Richte 389 Verzeichnisserver ein"
 
-#. TODO: Get the return code and display output if not successful.
-#: ../pykolab/setup/setup_ldap.py:253
+#: ../pykolab/setup/setup_ldap.py:356
+msgid ""
+"\n"
+"                        An error was detected in the setup procedure for 389\n"
+"                        Directory Server. This setup will write out stderr and\n"
+"                        stdout to /var/log/kolab/setup.error.log and\n"
+"                        /var/log/kolab/setup.out.log respectively, before it\n"
+"                        exits.\n"
+"                    "
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:373
 msgid "Setup DS stdout:"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:256
+#: ../pykolab/setup/setup_ldap.py:376
 msgid "Setup DS stderr:"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:279 ../pykolab/setup/setup_mysql.py:129
-msgid "Could not find the Kolab schema file"
-msgstr "Konnte Kolab Schema Datei nicht finden"
+#: ../pykolab/setup/setup_ldap.py:402
+msgid "Could not copy the LDAP extensions for Kolab"
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:405
+msgid "Could not find the ldap Kolab schema file"
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:417
+msgid "Could not start the directory server service."
+msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:293
+#: ../pykolab/setup/setup_ldap.py:431
 msgid ""
 "\n"
 "                        Please supply a Cyrus Administrator password. This\n"
@@ -1708,11 +2310,11 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:303
+#: ../pykolab/setup/setup_ldap.py:441
 msgid "Cyrus Administrator password"
 msgstr "Cyrus Administrator Passwort"
 
-#: ../pykolab/setup/setup_ldap.py:310
+#: ../pykolab/setup/setup_ldap.py:448
 msgid ""
 "\n"
 "                        Please supply a Kolab Service account password. This\n"
@@ -1722,98 +2324,127 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:319
+#: ../pykolab/setup/setup_ldap.py:457
 msgid "Kolab Service password"
 msgstr "Kolab-Dienst-Passwort"
 
-#: ../pykolab/setup/setup_ldap.py:329
+#: ../pykolab/setup/setup_ldap.py:467
 msgid "Writing out configuration to kolab.conf"
 msgstr "Schreibe Einstellungen in kolab.conf"
 
-#: ../pykolab/setup/setup_ldap.py:343
+#: ../pykolab/setup/setup_ldap.py:481
 msgid "Inserting service users into LDAP."
 msgstr "Füge Service-Benutzer ins LDAP ein."
 
-#: ../pykolab/setup/setup_ldap.py:417
+#: ../pykolab/setup/setup_ldap.py:555
 msgid "Writing out cn=kolab,cn=config"
 msgstr "Schreibe cn=kolab,cn=config"
 
 #. TODO: Add kolab-admin role
 #. TODO: Assign kolab-admin admin ACLs
-#: ../pykolab/setup/setup_ldap.py:441
+#: ../pykolab/setup/setup_ldap.py:579
 #, python-format
 msgid "Adding domain %s to list of domains for this deployment"
 msgstr "Füge Domain %s zu dieser Installation hinzu"
 
-#: ../pykolab/setup/setup_ldap.py:457
+#: ../pykolab/setup/setup_ldap.py:607
 msgid "Disabling anonymous binds"
 msgstr "Stelle anonymes Binden ab"
 
 #. TODO: Ensure the uid attribute is unique
 #. TODO^2: Consider renaming the general "attribute uniqueness to "uid
 #. attribute uniqueness"
-#: ../pykolab/setup/setup_ldap.py:465
+#: ../pykolab/setup/setup_ldap.py:615
 msgid "Enabling attribute uniqueness plugin"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:471
+#: ../pykolab/setup/setup_ldap.py:621
 msgid "Enabling referential integrity plugin"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:477
+#: ../pykolab/setup/setup_ldap.py:627
 msgid "Enabling and configuring account policy plugin"
 msgstr ""
 
 #. TODO: Add kolab-admin role
-#: ../pykolab/setup/setup_ldap.py:492
+#: ../pykolab/setup/setup_ldap.py:642
 msgid "Adding the kolab-admin role"
 msgstr "Füge Kolab-Admin Rolle hinzu"
 
 #. TODO: User writeable attributes on root_dn
-#: ../pykolab/setup/setup_ldap.py:503
+#: ../pykolab/setup/setup_ldap.py:653
 #, python-format
 msgid "Setting access control to %s"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:40
+#: ../pykolab/setup/setup_ldap.py:679
+msgid "Could not start and configure to start on boot, the "
+msgstr ""
+
+#: ../pykolab/setup/setup_mta.py:41
 msgid "Setup MTA."
 msgstr "Richte MTA ein."
 
-#: ../pykolab/setup/setup_mta.py:224 ../pykolab/setup/setup_php.py:80
+#: ../pykolab/setup/setup_mta.py:317 ../pykolab/setup/setup_php.py:106
 #, python-format
 msgid "Setting key %r to %r"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:252
+#: ../pykolab/setup/setup_mta.py:350
 msgid "Could not write out Postfix configuration file /etc/postfix/master.cf"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:287
-msgid ""
-"Could not write out Amavis configuration file /etc/amavisd/amavisd.conf"
+#: ../pykolab/setup/setup_mta.py:397
+msgid "Could not write out Amavis configuration file amavisd.conf"
+msgstr ""
+
+#: ../pykolab/setup/setup_mta.py:405
+msgid "Not writing out any configuration for Amavis."
+msgstr ""
+
+#: ../pykolab/setup/setup_mta.py:437
+msgid "Could not start the postfix, clamav and amavisd services services."
 msgstr ""
 
 #: ../pykolab/setup/setup_mysql.py:39
 msgid "Setup MySQL."
 msgstr "Richte MySQL ein."
 
-#: ../pykolab/setup/setup_mysql.py:53
+#: ../pykolab/setup/setup_mysql.py:49
+msgid "Could not start the MySQL database service."
+msgstr ""
+
+#: ../pykolab/setup/setup_mysql.py:71
+msgid "What MySQL server are we setting up?"
+msgstr ""
+
+#: ../pykolab/setup/setup_mysql.py:75
 msgid ""
 "\n"
-"                    Please supply a root password for MySQL. This password will\n"
-"                    be the administrative user for this MySQL server, and it\n"
-"                    should be kept a secret. After this setup process has\n"
-"                    completed, Kolab is going to discard and forget about this\n"
-"                    password, but you will need it for administrative tasks in\n"
-"                    MySQL.\n"
-"                "
+"                        Please supply the root password for MySQL, so we can set\n"
+"                        up user accounts for other components that use MySQL.\n"
+"                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:64
+#: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99
+#: ../pykolab/setup/setup_roundcube.py:183
+#: ../pykolab/setup/setup_syncroton.py:63
 msgid "MySQL root password"
+msgstr "MySQL root Password"
+
+#: ../pykolab/setup/setup_mysql.py:88
+msgid ""
+"\n"
+"                        Please supply a root password for MySQL. This password\n"
+"                        will be the administrative user for this MySQL server,\n"
+"                        and it should be kept a secret. After this setup process\n"
+"                        has completed, Kolab is going to discard and forget\n"
+"                        about this password, but you will need it for\n"
+"                        administrative tasks in MySQL.\n"
+"                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:103
+#: ../pykolab/setup/setup_mysql.py:139
 msgid ""
 "\n"
 "                        Please supply a password for the MySQL user 'kolab'.\n"
@@ -1822,8 +2453,12 @@ msgid ""
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:111
+#: ../pykolab/setup/setup_mysql.py:147
 msgid "MySQL kolab password"
+msgstr "MySQL-kolab Password"
+
+#: ../pykolab/setup/setup_mysql.py:165
+msgid "Could not find the MySQL Kolab schema file"
 msgstr ""
 
 #: ../pykolab/setup/setup_php.py:42
@@ -1832,23 +2467,38 @@ msgstr "PHP Optionen"
 
 #: ../pykolab/setup/setup_php.py:49
 msgid "Specify the timezone for PHP."
+msgstr "Zeitzone für PHP definieren."
+
+#: ../pykolab/setup/setup_php.py:57
+msgid "Specify the path to the php.ini file used with the webserver."
 msgstr ""
 
-#: ../pykolab/setup/setup_php.py:53
+#: ../pykolab/setup/setup_php.py:61
 msgid "Setup PHP."
 msgstr "Richte PHP ein."
 
-#: ../pykolab/setup/setup_php.py:58
+#: ../pykolab/setup/setup_php.py:66
 msgid ""
 "\n"
 "                        Please supply the timezone PHP should be using.\n"
+"                        You have to use a Continent or Country / City locality name\n"
+"                        like 'Europe/Berlin', but not just 'CEST'.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_php.py:64
+#: ../pykolab/setup/setup_php.py:74
 msgid "Timezone ID"
 msgstr "Zeitzonen ID"
 
+#: ../pykolab/setup/setup_php.py:80
+#, python-format
+msgid "Cannot configure PHP through %r (No such file or directory)"
+msgstr ""
+
+#: ../pykolab/setup/setup_php.py:91
+msgid "Could not find PHP configuration file php.ini"
+msgstr ""
+
 #: ../pykolab/setup/setup_roundcube.py:44
 msgid "Setup Roundcube."
 msgstr "Stelle Roundcube ein."
@@ -1862,18 +2512,29 @@ msgid ""
 "                "
 msgstr ""
 
-#: ../pykolab/setup/setup_syncroton.py:40
-msgid "Setup Syncroton."
-msgstr "Richte Syncroton ein."
+#: ../pykolab/setup/setup_roundcube.py:56
+msgid "MySQL roundcube password"
+msgstr "MySQL roundcube Passwort"
 
-#: ../pykolab/setup/setup_zpush.py:41
-msgid "Setup zpush."
-msgstr "zpush einrichten."
+#: ../pykolab/setup/setup_roundcube.py:120
+#, python-format
+msgid "Using template file %r"
+msgstr ""
 
-#: ../pykolab/setup/setup_zpush.py:45
-msgid "Z-Push is not installed on this system"
+#: ../pykolab/setup/setup_roundcube.py:127
+#, python-format
+msgid "Successfully compiled template %r, writing out to %r"
 msgstr ""
 
+#: ../pykolab/setup/setup_roundcube.py:228
+#: ../pykolab/setup/setup_syncroton.py:93
+msgid "Could not start the webserver server service."
+msgstr ""
+
+#: ../pykolab/setup/setup_syncroton.py:40
+msgid "Setup Syncroton."
+msgstr "Richte Syncroton ein."
+
 #. start_max = (int)(time.time())
 #: ../pykolab/telemetry.py:588
 #, python-format
@@ -1899,111 +2560,181 @@ msgstr ""
 msgid "No database available"
 msgstr "Keine Datenbank verfügbar"
 
-#: ../pykolab/utils.py:57 ../pykolab/utils.py:59
+#: ../pykolab/utils.py:62 ../pykolab/utils.py:64
 #, python-format
 msgid "Confirm %s: "
-msgstr ""
+msgstr "Bestätige %s:"
 
-#: ../pykolab/utils.py:62
+#: ../pykolab/utils.py:67
 msgid "Incorrect confirmation. "
-msgstr ""
+msgstr "Ungültige Bestätigung."
 
-#: ../pykolab/utils.py:67 ../pykolab/utils.py:72
+#: ../pykolab/utils.py:72 ../pykolab/utils.py:77
 #, python-format
 msgid "%s: "
 msgstr "%s:"
 
-#: ../pykolab/utils.py:69 ../pykolab/utils.py:74
+#: ../pykolab/utils.py:74 ../pykolab/utils.py:79
 #, python-format
 msgid "%s [%s]: "
 msgstr "%s [%s]: "
 
-#: ../pykolab/utils.py:119
+#: ../pykolab/utils.py:124
 msgid "Please answer 'yes' or 'no'."
 msgstr "Bitte antworten Sie mit 'yes' (ja) oder 'no' (nein)."
 
-#: ../pykolab/utils.py:185
+#: ../pykolab/utils.py:164
+msgid "Choice"
+msgstr ""
+
+#: ../pykolab/utils.py:167
+msgid "Choice (type '?' for options)"
+msgstr ""
+
+#: ../pykolab/utils.py:268
 #, python-format
 msgid "Could not change the permissions on %s"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:257
+#: ../pykolab/utils.py:479
+#, python-format
+msgid "Transliterating string %r with locale %r"
+msgstr ""
+
+#: ../pykolab/utils.py:487
+msgid "Attempting to set locale"
+msgstr ""
+
+#: ../pykolab/utils.py:489
+msgid "Success setting locale"
+msgstr ""
+
+#: ../pykolab/utils.py:491
+msgid "Failure to set locale"
+msgstr ""
+
+#: ../pykolab/utils.py:499
+#, python-format
+msgid "Executing '%s | %s'"
+msgstr ""
+
+#: ../pykolab/utils.py:510
+#, python-format
+msgid "Could not translate %s using locale %s"
+msgstr ""
+
+#: ../pykolab/wap_client/__init__.py:320
 #, python-format
 msgid "Requesting %r with params %r"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:263
+#: ../pykolab/wap_client/__init__.py:328
 #, python-format
 msgid "Got response: %r"
 msgstr ""
 
 #. Some data is not JSON
-#: ../pykolab/wap_client/__init__.py:268
+#: ../pykolab/wap_client/__init__.py:334
 msgid "Response data is not JSON"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:79 ../pykolab/xml/attendee.py:99
+#. support integer values, too
+#: ../pykolab/xml/attendee.py:9 ../pykolab/xml/attendee.py:17
+msgid "Needs Action"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:10 ../pykolab/xml/attendee.py:18
+msgid "Accepted"
+msgstr "Akzeptiert"
+
+#: ../pykolab/xml/attendee.py:11 ../pykolab/xml/attendee.py:19
+msgid "Declined"
+msgstr "Abgelehnt"
+
+#: ../pykolab/xml/attendee.py:12 ../pykolab/xml/attendee.py:20
+msgid "Tentatively Accepted"
+msgstr "Provisorisch Akzeptiert"
+
+#: ../pykolab/xml/attendee.py:13 ../pykolab/xml/attendee.py:21
+msgid "Delegated"
+msgstr "Delegiert"
+
+#: ../pykolab/xml/attendee.py:14
+msgid "Completed"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:15
+msgid "In Process"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130
 msgid "Not a valid attendee"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:84
+#: ../pykolab/xml/attendee.py:115
 msgid "No valid delegator references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:104
+#: ../pykolab/xml/attendee.py:135
 msgid "No valid delegatee references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:140
+#: ../pykolab/xml/attendee.py:180
 #, python-format
 msgid "Invalid cutype %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:151
+#: ../pykolab/xml/attendee.py:192
 #, python-format
 msgid "Invalid participant status %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:159
+#: ../pykolab/xml/attendee.py:200
 #, python-format
 msgid "Invalid role %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:172
+#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708
+#: ../pykolab/xml/event.py:751
+msgid "Event start needs datetime.date or datetime.datetime instance"
+msgstr ""
+
+#: ../pykolab/xml/event.py:241
 #, python-format
 msgid "No attendee with email or name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:180
+#: ../pykolab/xml/event.py:249
 #, python-format
 msgid "Invalid argument value attendee %r, must be basestring or Attendee"
 msgstr ""
 
-#: ../pykolab/xml/event.py:186
+#: ../pykolab/xml/event.py:255
 #, python-format
 msgid "No attendee with email %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:192
+#: ../pykolab/xml/event.py:261
 #, python-format
 msgid "No attendee with name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:338
+#: ../pykolab/xml/event.py:426
 msgid "Invalid participant status"
 msgstr ""
 
-#: ../pykolab/xml/event.py:538
-msgid "Event end needs datetime.date or datetime.datetime instance"
+#: ../pykolab/xml/event.py:542
+#, python-format
+msgid "Invalid status %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:654
+#: ../pykolab/xml/event.py:550
 #, python-format
-msgid "Invalid status %r"
+msgid "Invalid classification %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:675 ../pykolab/xml/event.py:725
-msgid "Event start needs datetime.date or datetime.datetime instance"
+#: ../pykolab/xml/event.py:577
+msgid "Event end needs datetime.date or datetime.datetime instance"
 msgstr ""
 
 #: ../pykolab/xml/event.py:761
@@ -2011,62 +2742,434 @@ msgstr ""
 msgid "Invalid status set: %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:879
+#: ../pykolab/xml/event.py:923
 msgid "No sender specified"
 msgstr ""
 
-#: ../saslauthd/__init__.py:126 ../saslauthd/__init__.py:134
-#: ../wallace/__init__.py:362 ../wallace/__init__.py:371
+#: ../pykolab/xml/event.py:932
+#, python-format
+msgid "Invitation for %s was %s"
+msgstr ""
+
+#: ../pykolab/xml/event.py:937
+msgid "This is an automated response to one of your event requests."
+msgstr ""
+
+#: ../saslauthd/__init__.py:99
+#, python-format
+msgid "Could not create %r: %r"
+msgstr ""
+
+#: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145
+#: ../wallace/__init__.py:403 ../wallace/__init__.py:412
 msgid ""
 "Traceback occurred, please report a bug at http://bugzilla.kolabsys.com"
 msgstr "Ein Fehler mit Traceback trat auf, bitte legen Sie einen Bericht auf  http://bugzilla.kolabsys.com an"
 
-#: ../wallace/__init__.py:61
+#: ../saslauthd/__init__.py:185
+msgid "kolab-saslauthd could not accept "
+msgstr ""
+
+#: ../saslauthd/__init__.py:190
+msgid "Maximum tries exceeded, exiting"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190
+#: ../wallace/module_resources.py:879
+#, python-format
+msgid "Reservation Request for %(summary)s was %(status)s"
+msgstr ""
+
+#. check notification message sent to resource owner (jane)
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:605
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:621
+#: ../wallace/module_resources.py:954
+#, python-format
+msgid "Booking for %s has been %s"
+msgstr "Buchung für %s wurde %s"
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:146
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:720
+#: ../wallace/module_invitationpolicy.py:374
+#, python-format
+msgid "\"%(summary)s\" has been %(status)s"
+msgstr "\"%(summary)s\" wurde %(status)s"
+
+#. check for notification message
+#. this notification should be suppressed until mark has replied, too
+#. this triggers an additional notification
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:616
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:622
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:635
+#: ../wallace/module_invitationpolicy.py:925
+#, python-format
+msgid "\"%s\" has been updated"
+msgstr "\"%s\" wurde aktualisiert"
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:627
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639
+msgid "PENDING"
+msgstr ""
+
+#: ../wallace/__init__.py:57
+#, python-format
+msgid "Wallace modules: %r"
+msgstr ""
+
+#: ../wallace/__init__.py:69
+#, python-format
+msgid "Module %s.execute() failed on message %r with error: %s"
+msgstr ""
+
+#: ../wallace/__init__.py:78
 #, python-format
 msgid "Worker process %s initializing"
 msgstr ""
 
-#: ../wallace/__init__.py:80
+#: ../wallace/__init__.py:100
 msgid "Bind address for Wallace."
-msgstr ""
+msgstr "Bind-Adresse für Wallace."
 
-#: ../wallace/__init__.py:106
+#: ../wallace/__init__.py:126
 msgid "Port that Wallace is supposed to use."
 msgstr "Port, den Wallace benutzen soll."
 
-#: ../wallace/__init__.py:157
+#: ../wallace/__init__.py:177
 #, python-format
 msgid "Could not bind to socket on port %d on bind "
 msgstr ""
 
-#: ../wallace/__init__.py:169
+#: ../wallace/__init__.py:189
 msgid "Could not shut down socket"
-msgstr ""
+msgstr "Konnte Socket nicht schließen"
 
-#: ../wallace/__init__.py:226
+#: ../wallace/__init__.py:253
 msgid "Accepted connection"
 msgstr "Verbindung akzeptiert"
 
-#: ../wallace/__init__.py:387
+#: ../wallace/__init__.py:428
 #, python-format
 msgid "Could not write pid file %s"
 msgstr ""
 
-#: ../wallace/module_optout.py:61 ../wallace/module_resources.py:94
+#: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60
+#: ../wallace/module_invitationpolicy.py:168 ../wallace/module_optout.py:61
+#: ../wallace/module_resources.py:120
 #, python-format
 msgid "Issuing callback after processing to stage %s"
 msgstr ""
 
-#: ../wallace/module_optout.py:62 ../wallace/module_resources.py:100
+#: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61
+#: ../wallace/module_invitationpolicy.py:170 ../wallace/module_optout.py:62
+#: ../wallace/module_resources.py:126
 #, python-format
 msgid "Testing cb_action_%s()"
 msgstr ""
 
-#: ../wallace/module_optout.py:64 ../wallace/module_resources.py:103
+#: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63
+#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:64
+#: ../wallace/module_resources.py:129
 #, python-format
 msgid "Attempting to execute cb_action_%s()"
 msgstr ""
 
+#: ../wallace/module_footer.py:67
+#, python-format
+msgid "Executing module footer for %r, %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:66
+#, python-format
+msgid "Executing module gpgencrypt for %r, %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:98
+msgid "Message is already encrypted (app/pgp-enc content-type)"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:102
+msgid "Message already encrypted by main content-type header"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:131
+msgid ""
+"Configured to encrypt to a key not configured, and strict policy enabled. "
+"Bailing out."
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:134
+msgid ""
+"Configured to encrypt to a key not configured, but continuing anyway (see "
+"'gpgencrypt_strict')."
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:171
+#, python-format
+msgid "Recipients: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:183
+#, python-format
+msgid "Current keys: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:188
+#, python-format
+msgid "Retrieving key for recipient: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:192 ../wallace/module_gpgencrypt.py:208
+#, python-format
+msgid "Found matching address %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:200
+#, python-format
+msgid "Found matching address %r in remote keys"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:232
+#, python-format
+msgid "An error occurred: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:154
+#, python-format
+msgid "Invitation policy called for %r, %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:211
+#: ../wallace/module_resources.py:169
+#, python-format
+msgid "Failed to parse iTip events from message: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:215
+msgid ""
+"Message is not an iTip message or does not contain any (valid) iTip events."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:219
+#, python-format
+msgid ""
+"iTip events attached to this message contain the following information: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:232
+#, python-format
+msgid "No itips, no users, pass along %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:235
+#, python-format
+msgid "iTips, but no users, pass along %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:255
+#, python-format
+msgid "No user attendee matching envelope recipient %s, skip message"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:259
+#, python-format
+msgid "Receiving user: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:284
+#, python-format
+msgid "Apply invitation policy %r for domain %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:295
+#, python-format
+msgid "Ignoring '%s' iTip method"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:299
+#, python-format
+msgid "iTip message %r consumed by the invitationpolicy module"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:315
+msgid "Pass invitation for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:320
+#, python-format
+msgid "Receiving Attendee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:339
+#, python-format
+msgid "Existing event: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:350
+#, python-format
+msgid "Precondition for event %r fulfilled: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:386
+#, python-format
+msgid "No RSVP for recipient %r requested"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:412
+msgid "Pass reply for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:419
+#, python-format
+msgid "Sender Attendee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:431
+#, python-format
+msgid ""
+"The iTip reply sequence (%r) doesn't match the referred event version (%r). "
+"Forwarding to Inbox."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:437
+#, python-format
+msgid "Auto-updating event %r on iTip REPLY"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:459
+#: ../wallace/module_invitationpolicy.py:488
+msgid ""
+"The event referred by this reply was not found in the user's calendars. "
+"Forwarding to Inbox."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:472
+msgid "Pass cancellation for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:517
+#, python-format
+msgid "Checking if email address %r belongs to a local user"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:522
+#, python-format
+msgid "User DN: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:524
+#, python-format
+msgid "No user record(s) found for %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:577
+#, python-format
+msgid "User record doesn't have the mailbox attribute %r set"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:590
+#, python-format
+msgid "IMAP proxy authentication failed: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:612
+#, python-format
+msgid "List calendar folders for user %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:628
+#, python-format
+msgid "IMAP metadata for %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:658
+#, python-format
+msgid "Searching folder %r for event %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:670
+#: ../wallace/module_invitationpolicy.py:709
+#: ../wallace/module_resources.py:486
+#, python-format
+msgid "Failed to parse event from message %s/%s: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:696
+#, python-format
+msgid "Listing events from folder %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:715
+#, python-format
+msgid "Existing event %r conflicts with invitation %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:722
+#: ../wallace/module_resources.py:344
+#, python-format
+msgid "start: %r, end: %r, total: %r, messages: %d"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:748
+#, python-format
+msgid "%r is locked, waiting..."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:811
+#, python-format
+msgid "Failed to save event: no calendar folder found for user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:814
+#, python-format
+msgid "Save event %r to user calendar %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:827
+#, python-format
+msgid "Failed to save event to user calendar at %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:843
+#, python-format
+msgid "Delete event %r in %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:863
+#, python-format
+msgid "Compose participation status summary for event %r to user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:901
+#, python-format
+msgid ""
+"Waiting for more automated replies (got %d of %d); skipping notification"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:998
+#, python-format
+msgid "Updated %s's copy of %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1001
+#, python-format
+msgid "Attendee %s's copy of %r not found"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1004
+#, python-format
+msgid "Attendee %r not found in LDAP"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1008
+#, python-format
+msgid ""
+"\n"
+"        %(name)s has %(status)s your invitation for %(summary)s.\n"
+"\n"
+"        *** This is an automated response sent by the Kolab Invitation system ***\n"
+"    "
+msgstr ""
+
 #. modules.next_module('optout')
 #: ../wallace/module_optout.py:70
 #, python-format
@@ -2088,210 +3191,295 @@ msgstr ""
 msgid "Could not send request to optout_url %s"
 msgstr ""
 
-#: ../wallace/module_resources.py:81
+#: ../wallace/module_resources.py:110
 #, python-format
 msgid "Resource Management called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:139
+#: ../wallace/module_resources.py:174
 msgid "Message is not an iTip message or does not contain any "
 msgstr ""
 
-#: ../wallace/module_resources.py:147
+#: ../wallace/module_resources.py:182
 msgid "iTip events attached to this message contain the "
 msgstr ""
 
-#: ../wallace/module_resources.py:174
+#: ../wallace/module_resources.py:205
 msgid "Not an iTip message, but sent to resource nonetheless. Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:182
-msgid "No itips, no resources, pass along"
+#: ../wallace/module_resources.py:213
+#, python-format
+msgid "No itips, no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:186
-msgid "iTips, but no resources, pass along"
+#: ../wallace/module_resources.py:216
+#, python-format
+msgid "iTips, but no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:218
+#: ../wallace/module_resources.py:225
 #, python-format
-msgid "Resources: %r"
+msgid "No resource attendees matching envelope recipient %s, Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:236
+#: ../wallace/module_resources.py:234
 #, python-format
-msgid "Checking events in resource folder %r"
+msgid "Resources: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:243
+#: ../wallace/module_resources.py:244
 #, python-format
-msgid "Mailbox for resource %r doesn't exist"
+msgid "Receiving Resource: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:256
+#: ../wallace/module_resources.py:252
 #, python-format
-msgid "Fetching message UID %r from folder %r"
+msgid "Recipient %r is non-participant, ignoring message"
 msgstr ""
 
-#: ../wallace/module_resources.py:295
+#: ../wallace/module_resources.py:279
 #, python-format
-msgid "Event %r conflicts with event "
+msgid "Accept invitation for individual resource %r / %r"
 msgstr ""
 
 #: ../wallace/module_resources.py:308
 #, python-format
-msgid "start: %r, end: %r, total: %r, messages: %r"
+msgid "Delegate invitation for resource collection %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:315
+#: ../wallace/module_resources.py:340
+#, python-format
+msgid "Failed to read resource calendar for %r: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:350
 #, python-format
 msgid "Polling for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:319
+#: ../wallace/module_resources.py:353
 #, python-format
 msgid "Resource %r has been popped from the list"
 msgstr ""
 
-#: ../wallace/module_resources.py:326
+#: ../wallace/module_resources.py:357
 msgid "Resource is a collection"
 msgstr ""
 
-#: ../wallace/module_resources.py:374 ../wallace/module_resources.py:424
+#: ../wallace/module_resources.py:368
 #, python-format
-msgid "Adding event to %r"
+msgid "Removed conflicting resources from %r: (%r) => %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:473
+#: ../wallace/module_resources.py:380
 #, python-format
-msgid "Method %r not really interesting for us."
+msgid "Conflicting events: %r for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:481
+#: ../wallace/module_resources.py:397
 #, python-format
-msgid "Raw iTip payload: %s"
+msgid "Delegate to another resource collection member: %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:491
-msgid "Could not read iTip from message."
+#: ../wallace/module_resources.py:459
+#, python-format
+msgid "Checking events in resource folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:513
-msgid "iTip event without a start"
+#: ../wallace/module_resources.py:475
+#, python-format
+msgid "Fetching message UID %r from folder %r"
 msgstr ""
 
-#. end if c.name == "VEVENT"
-#. end for c in cal.walk()
-#. end if part.get_content_type() == "text/calendar"
-#. end for part in message.walk()
-#. if message.is_multipart()
-#: ../wallace/module_resources.py:543
-msgid "Message is not an iTip message (non-multipart message)"
+#: ../wallace/module_resources.py:498
+#, python-format
+msgid "Event %r conflicts with event %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:564
+#: ../wallace/module_resources.py:525
 #, python-format
-msgid "Checking if email address %r belongs to a resource (collection)"
+msgid "Adding event to %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:575 ../wallace/module_resources.py:649
-#: ../wallace/module_resources.py:699
+#: ../wallace/module_resources.py:573
 #, python-format
-msgid "No resource (collection) records found for %r"
+msgid "Failed to save event to resource calendar at %r: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:590
+#, python-format
+msgid "Delete resource calendar object %r in %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:583 ../wallace/module_resources.py:657
-#: ../wallace/module_resources.py:707
+#: ../wallace/module_resources.py:633
+#, python-format
+msgid "Checking if email address %r belongs to a resource (collection)"
+msgstr ""
+
+#: ../wallace/module_resources.py:641 ../wallace/module_resources.py:709
+#: ../wallace/module_resources.py:743
 #, python-format
 msgid "Resource record(s): %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:589 ../wallace/module_resources.py:664
-#: ../wallace/module_resources.py:714
+#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711
+#: ../wallace/module_resources.py:746
+#, python-format
+msgid "No resource (collection) records found for %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:647 ../wallace/module_resources.py:715
+#: ../wallace/module_resources.py:750
 #, python-format
 msgid "Resource record: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:608
+#: ../wallace/module_resources.py:667
 #, python-format
 msgid "Raw itip_events: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:616
+#: ../wallace/module_resources.py:675
 #, python-format
 msgid "Raw set of attendees: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:624
+#: ../wallace/module_resources.py:683
 #, python-format
 msgid "Raw set of resources: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:638
+#: ../wallace/module_resources.py:702
 #, python-format
 msgid "Checking if attendee %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:671 ../wallace/module_resources.py:717
+#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752
 msgid "Resource reservation made but no resource records found"
 msgstr ""
 
-#: ../wallace/module_resources.py:689
+#: ../wallace/module_resources.py:737
 #, python-format
 msgid "Checking if resource %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:721
+#: ../wallace/module_resources.py:755
 msgid "The following resources are being referred to in the "
 msgstr ""
 
+#: ../wallace/module_resources.py:894
+#, python-format
+msgid ""
+"\n"
+"                *** This is an automated response, please do not reply! ***\n"
+"\n"
+"                Your reservation was delegated to \"%s\" which is available for the requested time.\n"
+"            "
+msgstr ""
+
+#: ../wallace/module_resources.py:905
+#, python-format
+msgid ""
+"\n"
+"        *** This is an automated response, please do not reply! ***\n"
+"        \n"
+"        We hereby inform you that your reservation was %s.\n"
+"    "
+msgstr ""
+
+#: ../wallace/module_resources.py:912
+#, python-format
+msgid ""
+"\n"
+"            If you have questions about this reservation, please contact\n"
+"            %s <%s> %s\n"
+"        "
+msgstr ""
+
+#: ../wallace/module_resources.py:941
+#, python-format
+msgid "Sending booking notification for event %r to %r from %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:954
+msgid "failed"
+msgstr ""
+
+#: ../wallace/module_resources.py:973
+#, python-format
+msgid ""
+"\n"
+"            The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s.\n"
+"\n"
+"            *** This is an automated message, sent to you as the resource owner. ***\n"
+"        "
+msgstr ""
+
+#: ../wallace/module_resources.py:979
+#, python-format
+msgid ""
+"\n"
+"            A reservation request for %(resource)s could not be processed automatically.\n"
+"            Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s.\n"
+"\n"
+"            *** This is an automated message, sent to you as the resource owner. ***\n"
+"        "
+msgstr ""
+
 #. This is a nested module
-#: ../wallace/modules.py:96
+#: ../wallace/modules.py:97
 #, python-format
 msgid "Module Group: %s"
+msgstr "Modulgruppe: %s"
+
+#: ../wallace/modules.py:108
+#, python-format
+msgid "No such module %r in modules %r (1)."
 msgstr ""
 
-#: ../wallace/modules.py:107 ../wallace/modules.py:112
-msgid "No such module."
+#: ../wallace/modules.py:113
+#, python-format
+msgid "No such module %r in modules %r (2)."
 msgstr ""
 
-#: ../wallace/modules.py:118
+#: ../wallace/modules.py:119
 #, python-format
 msgid "Holding message in queue for manual review (%s by %s)"
-msgstr ""
+msgstr "Behalte Nachricht zur manuellen Prüfung in der Warteliste (%s von %s)"
 
-#: ../wallace/modules.py:121
+#: ../wallace/modules.py:122
 #, python-format
 msgid "Deferring message in %s (by module %s)"
 msgstr ""
 
-#: ../wallace/modules.py:131
+#: ../wallace/modules.py:134
 #, python-format
 msgid "The time when the message was sent: %r"
-msgstr ""
+msgstr "Zeitpunkt des Versandts: %r"
 
-#: ../wallace/modules.py:132
+#: ../wallace/modules.py:135
 #, python-format
 msgid "The time now: %r"
 msgstr "Die Zeit ist jetzt: %r"
 
-#: ../wallace/modules.py:133
+#: ../wallace/modules.py:136
 #, python-format
 msgid "The time delta: %r"
-msgstr ""
+msgstr "Die Zeitdifferenz: %r"
 
 #. TODO: Send NDR back to user
-#: ../wallace/modules.py:137
+#: ../wallace/modules.py:140
 #, python-format
 msgid "Message in file %s older then 5 days, deleting"
 msgstr ""
 
-#: ../wallace/modules.py:162
+#: ../wallace/modules.py:165
 #, python-format
 msgid "Rejecting message in %s (by module %s)"
 msgstr ""
 
-#: ../wallace/modules.py:180
+#: ../wallace/modules.py:186
 #, python-format
 msgid ""
 "This is the email system Wallace at %s.\n"
@@ -2304,21 +3492,31 @@ msgid ""
 "Your message is being delivered to any other recipients you may have\n"
 "sent your message to. There is no need to resend the message to those\n"
 "recipients.\n"
-msgstr ""
+msgstr "Ich bin das E-Mail-System Wallace auf %s.\n\nMit Bedauern muß ich Sie informieren, daß die angehängte Nachricht\nnicht an die folgenden Empfänger zugestellt werden konnte:\n\n- %s\n\nFalls Sie noch andere Empfänger angegeben haben wurde die Nachricht\nan diese zugestellt. An diese Empfänger müssen sie die Nachricht\nnicht erneut senden.\n"
 
-#: ../wallace/modules.py:195
+#: ../wallace/modules.py:201
 #, python-format
 msgid ""
 "X-Wallace-Module: %s\n"
 "X-Wallace-Result: REJECT\n"
 msgstr ""
 
-#: ../wallace/modules.py:248
+#: ../wallace/modules.py:260
 #, python-format
 msgid "Accepting message in %s (by module %s)"
 msgstr ""
 
-#: ../wallace/modules.py:316
+#: ../wallace/modules.py:262
 #, python-format
-msgid "Module '%s' already registered"
+msgid "Accepting message in: %r"
 msgstr ""
+
+#: ../wallace/modules.py:269
+#, python-format
+msgid "recipients: %r"
+msgstr ""
+
+#: ../wallace/modules.py:347
+#, python-format
+msgid "Module '%s' already registered"
+msgstr "Modul '%s' ist bereits registriert"
diff --git a/po/de_DE.po b/po/de_DE.po
index e849ee9..b8bcc6f 100644
--- a/po/de_DE.po
+++ b/po/de_DE.po
@@ -1,678 +1,913 @@
 # SOME DESCRIPTIVE TITLE.
 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
 # This file is distributed under the same license as the PACKAGE package.
-#
+# 
 # Translators:
-#   <grote at kolabsys.com>, 2012.
+# Christoph Wickert <christoph.wickert at gmail.com>, 2012
+# Grote <grote at kolabsys.com>, 2012
+# Thomas Brüderli <roundcube at gmail.com>, 2014
 msgid ""
 msgstr ""
 "Project-Id-Version: Kolab Groupware Solution\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-01-12 11:17+0000\n"
-"PO-Revision-Date: 2012-08-14 11:13+0000\n"
-"Last-Translator: Jeroen van Meeuwen <vanmeeuwen at kolabsys.com>\n"
-"Language-Team: German (Germany) (http://www.transifex.com/projects/p/kolab/"
-"language/de_DE/)\n"
-"Language: de_DE\n"
+"POT-Creation-Date: 2014-07-10 07:21-0400\n"
+"PO-Revision-Date: 2014-07-22 13:04+0000\n"
+"Last-Translator: Thomas Brüderli <roundcube at gmail.com>\n"
+"Language-Team: German (Germany) (http://www.transifex.com/projects/p/kolab/language/de_DE/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"Language: de_DE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: ../bin/kolab_smtp_access_policy.py:206
+#: ../bin/kolab_smtp_access_policy.py:209
 #, python-format
 msgid "Adding policy request to instance %s"
 msgstr "Füge Richtlinien-Anfrage zu Instanz %s hinzu"
 
-#: ../bin/kolab_smtp_access_policy.py:446
+#: ../bin/kolab_smtp_access_policy.py:479
 msgid "Unauthorized access not allowed"
 msgstr "Unberechtigter Zugriff nicht erlaubt"
 
-#: ../bin/kolab_smtp_access_policy.py:475
-#: ../bin/kolab_smtp_access_policy.py:657
+#: ../bin/kolab_smtp_access_policy.py:508
+#: ../bin/kolab_smtp_access_policy.py:689
 msgid "Could not find recipient"
 msgstr "Konnte den Empfänger nicht finden"
 
-#: ../bin/kolab_smtp_access_policy.py:494
-#: ../bin/kolab_smtp_access_policy.py:594
+#: ../bin/kolab_smtp_access_policy.py:527
 #, python-format
-msgid "Could not find envelope sender user %s"
-msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden"
+msgid "Could not find envelope sender user %s (511)"
+msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:537
+#: ../bin/kolab_smtp_access_policy.py:570
 #, python-format
 msgid "Obtained authenticated user details for %r: %r"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:616
+#: ../bin/kolab_smtp_access_policy.py:627
+#, python-format
+msgid "Could not find envelope sender user %s"
+msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden"
+
+#: ../bin/kolab_smtp_access_policy.py:649
 #, python-format
 msgid "%s is unauthorized to send on behalf of %s"
 msgstr "Benutzer %s ist nicht berechtigt als Benutzer %s zu senden"
 
-#: ../bin/kolab_smtp_access_policy.py:626
+#: ../bin/kolab_smtp_access_policy.py:659
 #, python-format
-msgid "User %s attempted to use envelope sender address %s "
+msgid ""
+"User %s attempted to use envelope sender address %s without authorization"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:681
-#: ../bin/kolab_smtp_access_policy.py:692
+#: ../bin/kolab_smtp_access_policy.py:713
+#: ../bin/kolab_smtp_access_policy.py:724
 #, python-format
 msgid "Found user %s to be a delegate user of %s"
 msgstr "Benutzer %s ist ein delegierter Benutzer von %s"
 
-#: ../bin/kolab_smtp_access_policy.py:716
+#: ../bin/kolab_smtp_access_policy.py:748
 #, python-format
-msgid "Verifying authenticated sender '%(sender)s' with "
+msgid ""
+"Verifying authenticated sender '%(sender)s' with sasl_username "
+"'%(sasl_username)s' for recipient '%(recipient)s'"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:721
+#: ../bin/kolab_smtp_access_policy.py:751
 #, python-format
-msgid "Verifying unauthenticated sender '%(sender)s' "
+msgid ""
+"Verifying unauthenticated sender '%(sender)s' for recipient '%(recipient)s'"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:738
+#: ../bin/kolab_smtp_access_policy.py:767
 #, python-format
-msgid "Reproducing verify_recipient(%s, %s) from "
+msgid "Reproducing verify_recipient(%s, %s) from cache"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:760
+#: ../bin/kolab_smtp_access_policy.py:804
 #, python-format
 msgid "Using authentication domain %s instead of %s"
 msgstr "Benutze Authentisierungsdomain %s anstelle von %s"
 
-#: ../bin/kolab_smtp_access_policy.py:770
+#: ../bin/kolab_smtp_access_policy.py:814
 #, python-format
 msgid "Domain %s is a primary domain"
 msgstr "Die Domain %s ist die primäre Domain"
 
-#: ../bin/kolab_smtp_access_policy.py:778
+#: ../bin/kolab_smtp_access_policy.py:822
 #, python-format
-msgid "Checking the recipient for domain %s that is not "
+msgid ""
+"Checking the recipient for domain %s that is not ours. This is probably a "
+"configuration error."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:794
-msgid "This recipient address is related to multiple "
+#: ../bin/kolab_smtp_access_policy.py:837
+msgid ""
+"This recipient address is related to multiple object entries and the SMTP "
+"Access Policy can therefore not restrict message flow"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:803
+#: ../bin/kolab_smtp_access_policy.py:854
 #, python-format
-msgid "Recipient address %r not found. Allowing since "
+msgid ""
+"Recipient address %r not found. Allowing since the MTA was configured to "
+"accept the recipient."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:831
+#: ../bin/kolab_smtp_access_policy.py:890
 msgid "Invalid recipient"
 msgstr "Ungültiger Empfänger"
 
-#: ../bin/kolab_smtp_access_policy.py:842
+#: ../bin/kolab_smtp_access_policy.py:901
 msgid "Could not find this user, accepting"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:905
-#: ../bin/kolab_smtp_access_policy.py:958
+#: ../bin/kolab_smtp_access_policy.py:974
+#: ../bin/kolab_smtp_access_policy.py:1050
 #, python-format
-msgid "Sender %s is not allowed to send to "
+msgid "Sender %s is not allowed to send to recipient %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:945
+#: ../bin/kolab_smtp_access_policy.py:1038
 #, python-format
-msgid "Reproducing verify_sender(%r) from cache, "
+msgid "Reproducing verify_sender(%r) from cache"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:974
+#: ../bin/kolab_smtp_access_policy.py:1055
+msgid "Unverifiable sender."
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1060
+msgid "Sender is not using an alias"
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1068
 msgid "Sender uses unauthorized envelope sender address"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:991
+#: ../bin/kolab_smtp_access_policy.py:1085
 msgid "Could not verify sender"
 msgstr "Konnte den Absender nicht verifizieren"
 
-#: ../bin/kolab_smtp_access_policy.py:998
-msgid "Verifying whether sender is allowed to send to "
+#: ../bin/kolab_smtp_access_policy.py:1092
+msgid ""
+"Verifying whether sender is allowed to send to recipient using sender policy"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1012
+#: ../bin/kolab_smtp_access_policy.py:1105
 #, python-format
 msgid "Result is %r"
 msgstr "Das Ergebnis ist %r"
 
-#: ../bin/kolab_smtp_access_policy.py:1017
+#: ../bin/kolab_smtp_access_policy.py:1110
 msgid "No recipient policy restrictions exist for this sender"
 msgstr "Es existiert keine Empfängerrichtlinie für diesen Absender"
 
-#: ../bin/kolab_smtp_access_policy.py:1026
+#: ../bin/kolab_smtp_access_policy.py:1119
 msgid "Found a recipient policy to apply for this sender."
 msgstr "Empfänger-Richtlinie für diesen Benutzer gefunden"
 
-#: ../bin/kolab_smtp_access_policy.py:1041
+#: ../bin/kolab_smtp_access_policy.py:1134
 #, python-format
-msgid "Sender %s not allowed to send to recipient "
+msgid "Sender %s not allowed to send to recipient %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1063
+#: ../bin/kolab_smtp_access_policy.py:1155
 msgid "Cleaning up the cache"
 msgstr "Aufräumen des Cache"
 
-#: ../bin/kolab_smtp_access_policy.py:1085
+#: ../bin/kolab_smtp_access_policy.py:1177
 msgid ""
 "The 'uri' setting in the kolab_smtp_access_policy section is soon going to "
 "be deprecated in favor of 'cache_uri'"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1101
+#: ../bin/kolab_smtp_access_policy.py:1193
 #, python-format
 msgid "Operational Error in caching: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1152
+#: ../bin/kolab_smtp_access_policy.py:1245
 #, python-format
 msgid "Caching the policy result with timestamp %d"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1229
+#: ../bin/kolab_smtp_access_policy.py:1319
 #, python-format
 msgid "Returning action DEFER_IF_PERMIT: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1234
+#: ../bin/kolab_smtp_access_policy.py:1324
 #, python-format
 msgid "Returning action DUNNO: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1239
+#: ../bin/kolab_smtp_access_policy.py:1329
 #, python-format
 msgid "Returning action HOLD: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1244
+#: ../bin/kolab_smtp_access_policy.py:1334
 #, python-format
 msgid "Returning action PERMIT: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1249
+#: ../bin/kolab_smtp_access_policy.py:1459
 #, python-format
 msgid "Returning action REJECT: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1298
+#: ../bin/kolab_smtp_access_policy.py:1505
 msgid "Starting to loop for new request"
 msgstr "Starte Schleife für neue Anfrage"
 
-#: ../bin/kolab_smtp_access_policy.py:1305
+#: ../bin/kolab_smtp_access_policy.py:1512
 msgid "Timeout for policy request reading exceeded"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1311
+#: ../bin/kolab_smtp_access_policy.py:1518
 msgid "End of current request"
 msgstr "Ende der aktuellen Anfrage"
 
-#: ../bin/kolab_smtp_access_policy.py:1315
+#: ../bin/kolab_smtp_access_policy.py:1522
 #, python-format
 msgid "Getting line: %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1319
+#: ../bin/kolab_smtp_access_policy.py:1526
 msgid "Returning request"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1348
+#: ../bin/kolab_smtp_access_policy.py:1555
 msgid "Access Policy Options"
 msgstr "Zugriffsrichtlinien-Einstellungen"
 
-#: ../bin/kolab_smtp_access_policy.py:1355
+#: ../bin/kolab_smtp_access_policy.py:1562
 msgid "SMTP Policy request timeout."
 msgstr "Zeitüberschreitung der SMTP Richtlinien-Anfrage"
 
-#: ../bin/kolab_smtp_access_policy.py:1361
+#: ../bin/kolab_smtp_access_policy.py:1568
 msgid "Verify the recipient access policy."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1367
+#: ../bin/kolab_smtp_access_policy.py:1574
 msgid "Verify the sender access policy."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1373
+#: ../bin/kolab_smtp_access_policy.py:1580
 msgid "Allow unauthenticated senders."
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1385
+#: ../bin/kolab_smtp_access_policy.py:1594
 #, python-format
 msgid "Got request instance %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1394
+#: ../bin/kolab_smtp_access_policy.py:1603
 #, python-format
 msgid "Request instance %s is in state %s"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1402
+#: ../bin/kolab_smtp_access_policy.py:1611
 #, python-format
 msgid "Request instance %s is not yet in DATA state"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1414
+#: ../bin/kolab_smtp_access_policy.py:1623
 #, python-format
 msgid "Request instance %s reached DATA state"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1432
+#: ../bin/kolab_smtp_access_policy.py:1643
+#, python-format
+msgid "Unhandled exception caught: %r"
+msgstr ""
+
+#: ../bin/kolab_smtp_access_policy.py:1647
 msgid "Sender access denied"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1434
+#: ../bin/kolab_smtp_access_policy.py:1649
 msgid "Recipient access denied"
 msgstr ""
 
-#: ../bin/kolab_smtp_access_policy.py:1436
+#: ../bin/kolab_smtp_access_policy.py:1651
 msgid "No objections"
 msgstr ""
 
-#: ../conf.py:37 ../kolab.py:34 ../saslauthd.py:33
+#: ../conf.py:37 ../kolab-cli.py:34 ../saslauthd.py:33
 msgid "Cannot load pykolab/logger.py:"
 msgstr ""
 
-#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:48
-#: ../wallace/__init__.py:66
+#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:51
+#: ../wallace/__init__.py:85
 msgid "Daemon Options"
 msgstr ""
 
-#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:55
-#: ../wallace/__init__.py:73
+#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:58
+#: ../wallace/__init__.py:92
 msgid "Fork to the background."
 msgstr ""
 
-#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:64
-#: ../wallace/__init__.py:99
+#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:67
+#: ../wallace/__init__.py:118
 msgid "Path to the PID file to use."
 msgstr ""
 
-#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:73
-#: ../wallace/__init__.py:116
+#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76
+#: ../wallace/__init__.py:135
 msgid "Run as user USERNAME"
 msgstr ""
 
-#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:83
-#: ../wallace/__init__.py:90
+#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86
+#: ../wallace/__init__.py:109
 msgid "Run as group GROUPNAME"
 msgstr ""
 
-#: ../kolabd/__init__.py:122 ../pykolab/utils.py:180
-#: ../wallace/__init__.py:297
+#: ../kolabd/__init__.py:122 ../pykolab/logger.py:139 ../pykolab/utils.py:234
+#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:329
 #, python-format
 msgid "Group %s does not exist"
 msgstr ""
 
-#: ../kolabd/__init__.py:131 ../wallace/__init__.py:306
+#: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301
+#: ../wallace/__init__.py:338
 #, python-format
 msgid "Switching real and effective group id to %d"
 msgstr ""
 
-#: ../kolabd/__init__.py:153 ../pykolab/utils.py:204
-#: ../wallace/__init__.py:328
+#: ../kolabd/__init__.py:153 ../pykolab/logger.py:159 ../pykolab/utils.py:258
+#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:360
 #, python-format
 msgid "User %s does not exist"
 msgstr ""
 
-#: ../kolabd/__init__.py:163 ../wallace/__init__.py:338
+#: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333
+#: ../wallace/__init__.py:370
 #, python-format
 msgid "Switching real and effective user id to %d"
 msgstr ""
 
-#: ../kolabd/__init__.py:172 ../wallace/__init__.py:347
+#: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342
+#: ../wallace/__init__.py:379
 msgid "Could not change real and effective uid and/or gid"
 msgstr ""
 
-#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:122
-#: ../wallace/__init__.py:367
+#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133
+#: ../wallace/__init__.py:399
 msgid "Interrupted by user"
 msgstr ""
 
 #: ../kolabd/__init__.py:197 ../kolabd/__init__.py:208
-#: ../wallace/__init__.py:371 ../wallace/__init__.py:381
 msgid "Traceback occurred, please report a "
 msgstr ""
 
-#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:130
-#: ../wallace/__init__.py:377
+#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141
+#: ../wallace/__init__.py:408
 #, python-format
 msgid "Type Error: %s"
 msgstr ""
 
-#: ../kolabd/__init__.py:223 ../pykolab/auth/ldap/__init__.py:1623
+#: ../kolabd/__init__.py:230
+msgid "Could not connect to LDAP, is it running?"
+msgstr ""
+
+#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137
 #: ../pykolab/cli/cmd_sync.py:36
 msgid "Listing domains..."
+msgstr "Domänen werden geladen…"
+
+#: ../kolabd/__init__.py:244
+msgid "No domains. Not syncing"
 msgstr ""
 
-#: ../kolabd/__init__.py:260
+#: ../kolabd/__init__.py:275
 #, python-format
 msgid "added domains: %r, removed domains: %r"
 msgstr ""
 
-#: ../kolabd/process.py:48
+#: ../kolabd/process.py:33
+#, python-format
+msgid "Process created for domain %s"
+msgstr ""
+
+#: ../kolabd/process.py:42
 #, python-format
-msgid "Error in process %r, terminating: %r"
+msgid "Synchronizing for domain %s"
+msgstr ""
+
+#: ../kolabd/process.py:59
+#, python-format
+msgid ""
+"Error in process %r, terminating:\n"
+"\t%r"
 msgstr ""
 
 #: ../kolabd.py:31 ../setup-kolab.py:36 ../wallace.py:31
 msgid "Cannot load pykolab/constants.py:"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:94
+#: ../pykolab/auth/__init__.py:89
 #, python-format
 msgid "Called for domain %r"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:107 ../pykolab/auth/__init__.py:116
+#: ../pykolab/auth/__init__.py:106 ../pykolab/auth/__init__.py:115
 #, python-format
 msgid "Using section %s and domain %s"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:121
+#: ../pykolab/auth/__init__.py:120
 #, python-format
 msgid "Connecting to Authentication backend for domain %s"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:132
+#: ../pykolab/auth/__init__.py:131
 #, python-format
 msgid "Section %s has no option 'auth_mechanism'"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:139
+#: ../pykolab/auth/__init__.py:138
 #, python-format
 msgid "Section %s has auth_mechanism: %r"
 msgstr ""
 
-#: ../pykolab/auth/__init__.py:148 ../pykolab/auth/__init__.py:157
+#: ../pykolab/auth/__init__.py:147 ../pykolab/auth/__init__.py:156
 msgid "Starting LDAP..."
 msgstr ""
 
-#: ../pykolab/auth/ldap/cache.py:112
+#: ../pykolab/auth/ldap/cache.py:126
 #, python-format
 msgid "Inserting cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/cache.py:129
+#: ../pykolab/auth/ldap/cache.py:147
 #, python-format
 msgid "Updating timestamp for cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/cache.py:136
+#: ../pykolab/auth/ldap/cache.py:155
 #, python-format
 msgid "Updating result_attribute for cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:51
+#: ../pykolab/auth/ldap/__init__.py:52
 msgid "Python LDAP library does not support persistent search"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:142
+#: ../pykolab/auth/ldap/__init__.py:143
 #, python-format
 msgid "Attempting to authenticate user %s in realm %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:184
+#: ../pykolab/auth/ldap/__init__.py:175 ../pykolab/auth/ldap/__init__.py:226
+#, python-format
+msgid "Authentication cache failed: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:216 ../pykolab/auth/ldap/__init__.py:240
 #, python-format
 msgid "Binding with user_dn %s and password %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:194
+#: ../pykolab/auth/ldap/__init__.py:231 ../pykolab/auth/ldap/__init__.py:263
 #, python-format
 msgid "Failed to authenticate as user %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:211
-msgid "Connecting to LDAP..."
+#: ../pykolab/auth/ldap/__init__.py:249
+#, python-format
+msgid "Error occured, there is no such object: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:254
+msgid "Authentication cache failed to clear entry"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:215
+#: ../pykolab/auth/ldap/__init__.py:260
+#, python-format
+msgid "Exception occured: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:280
+msgid "Connecting to LDAP..."
+msgstr "Zum LDAP verbinden…"
+
+#: ../pykolab/auth/ldap/__init__.py:284
 #, python-format
 msgid "Attempting to use LDAP URI %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:363
+#: ../pykolab/auth/ldap/__init__.py:371
+#, python-format
+msgid "Entry ID: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:373
+#, python-format
+msgid "Entry DN: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:376
+#, python-format
+msgid ""
+"ldap search: (%r, %r, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:453
 #, python-format
 msgid "Finding recipient with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:431
+#: ../pykolab/auth/ldap/__init__.py:529
 #, python-format
 msgid "Finding resource with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:454
+#: ../pykolab/auth/ldap/__init__.py:560
 #, python-format
 msgid "Using timestamp %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:486
+#: ../pykolab/auth/ldap/__init__.py:595
+msgid "Applying recipient policy disabled through configuration"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:600
 #, python-format
 msgid "Applying recipient policy to %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:503
+#: ../pykolab/auth/ldap/__init__.py:617
 #, python-format
 msgid "Using mail attributes: %r, with primary %r and "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:514
+#: ../pykolab/auth/ldap/__init__.py:628
 #, python-format
 msgid "key %r not in entry"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:516
+#: ../pykolab/auth/ldap/__init__.py:630
 #, python-format
 msgid "key %r is the prim. mail attr."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:518
+#: ../pykolab/auth/ldap/__init__.py:632
 msgid "prim. mail pol. is not empty"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:521
+#: ../pykolab/auth/ldap/__init__.py:635
 #, python-format
 msgid "key %r is the sec. mail attr."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:523
+#: ../pykolab/auth/ldap/__init__.py:637
 msgid "sec. mail pol. is not empty"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:527 ../pykolab/auth/ldap/__init__.py:541
+#: ../pykolab/auth/ldap/__init__.py:641 ../pykolab/auth/ldap/__init__.py:655
 #, python-format
 msgid "Attributes %r are not yet available for entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:577
+#: ../pykolab/auth/ldap/__init__.py:694
 #, python-format
 msgid "No results for mail address %s found"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:588
+#: ../pykolab/auth/ldap/__init__.py:705
 #, python-format
 msgid "1 result for address %s found, verifying"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:598
+#: ../pykolab/auth/ldap/__init__.py:715
 #, python-format
 msgid "Too bad, primary email address %s "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:609 ../pykolab/auth/ldap/__init__.py:698
+#: ../pykolab/auth/ldap/__init__.py:726 ../pykolab/auth/ldap/__init__.py:815
 msgid "Address assigned to us"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:664
+#: ../pykolab/auth/ldap/__init__.py:781
 #, python-format
 msgid "No results for address %s found"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:675
+#: ../pykolab/auth/ldap/__init__.py:792
 #, python-format
 msgid "1 result for address %s found, "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:686
+#: ../pykolab/auth/ldap/__init__.py:803
 msgid "Too bad, secondary email "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:713
+#: ../pykolab/auth/ldap/__init__.py:830
 msgid "Recipient policy composed the following set of secondary "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:724
+#: ../pykolab/auth/ldap/__init__.py:841
 #, python-format
 msgid "Secondary mail addresses that we want is not None: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:735
+#: ../pykolab/auth/ldap/__init__.py:852
 msgid "Avoiding the duplication of the primary mail "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:746
+#: ../pykolab/auth/ldap/__init__.py:863
 #, python-format
 msgid "Entry is getting secondary mail addresses: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:754
+#: ../pykolab/auth/ldap/__init__.py:871
 msgid "Entry did not have any secondary mail "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:780
+#: ../pykolab/auth/ldap/__init__.py:888 ../pykolab/auth/ldap/__init__.py:894
+#, python-format
+msgid "secondary_mail_addresses: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:889 ../pykolab/auth/ldap/__init__.py:895
+#, python-format
+msgid "entry[%s]: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:906
 #, python-format
 msgid "Entry modifications list: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:800
+#: ../pykolab/auth/ldap/__init__.py:934
 #, python-format
 msgid "Setting entry attribute %r to %r for %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:836
+#: ../pykolab/auth/ldap/__init__.py:970
 #, python-format
-msgid "Could not update dn %r"
+msgid ""
+"Could not update dn %r:\n"
+"%r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:849
+#: ../pykolab/auth/ldap/__init__.py:983
 #, python-format
 msgid "Using filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:887
+#: ../pykolab/auth/ldap/__init__.py:998
+#, python-format
+msgid "Synchronization is searching against base DN: %s"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1044
 #, python-format
 msgid "About to consider the user quota for %r (used: %r, "
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:953
-#, fuzzy
-msgid "Invalid bind credentials"
-msgstr "Ungültiger Empfänger"
+#: ../pykolab/auth/ldap/__init__.py:1115
+msgid "Invalid DN, username and/or password."
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1236 ../pykolab/auth/ldap/__init__.py:1249
+#: ../pykolab/auth/ldap/__init__.py:1614 ../pykolab/auth/ldap/__init__.py:1627
+#, python-format
+msgid "Found a subject %r with access %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1356
+#, python-format
+msgid "Entry %s attribute value: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:1364
+#, python-format
+msgid "imap.user_mailbox_server(%r) result: %r"
+msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1248 ../pykolab/auth/ldap/__init__.py:1372
+#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853
 #, python-format
 msgid "Result from recipient policy: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1417
+#: ../pykolab/auth/ldap/__init__.py:1908
 #, python-format
 msgid "Kolab user %s does not have a result attribute %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1559
+#: ../pykolab/auth/ldap/__init__.py:2067
 #, python-format
 msgid "Finding domain root dn for domain %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1647
+#: ../pykolab/auth/ldap/__init__.py:2164
 msgid "Authentication database DOWN"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1731 ../pykolab/auth/ldap/__init__.py:1766
+#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296
 #, python-format
 msgid "Entry type: %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1854
+#: ../pykolab/auth/ldap/__init__.py:2321
+#, python-format
+msgid "Done with _synchronize_callback() for entry %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/__init__.py:2393
 msgid "LDAP Search Result Data Entry:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1870
+#: ../pykolab/auth/ldap/__init__.py:2409
 msgid "Entry Change Notification attributes:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1875
+#: ../pykolab/auth/ldap/__init__.py:2414
 #, python-format
 msgid "Change Type: %r (%r)"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1883
+#: ../pykolab/auth/ldap/__init__.py:2422
 #, python-format
 msgid "Previous DN: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1938
+#: ../pykolab/auth/ldap/__init__.py:2477
 #, python-format
 msgid "Object %s searched no longer exists"
-msgstr ""
+msgstr "Das gesuchte Objekt %s existiert nicht mehr"
 
-#: ../pykolab/auth/ldap/__init__.py:1948
+#: ../pykolab/auth/ldap/__init__.py:2487
 #, python-format
 msgid "%d results..."
-msgstr ""
+msgstr "%d Ergebnisse…"
 
-#: ../pykolab/auth/ldap/__init__.py:2051
+#: ../pykolab/auth/ldap/__init__.py:2590
 #, python-format
 msgid "Searching with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2095
+#: ../pykolab/auth/ldap/__init__.py:2642
 #, python-format
 msgid "Checking for support for %s on %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2114
+#: ../pykolab/auth/ldap/__init__.py:2661
 #, python-format
 msgid "Found support for %s"
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:36 ../pykolab/cli/cmd_create_mailbox.py:36
+#: ../pykolab/auth/ldap/__init__.py:2706
+#, python-format
+msgid "An error occured using %s: %r"
+msgstr ""
+
+#: ../pykolab/auth/ldap/syncrepl.py:46
+msgid "The name of the persistent, unique attribute "
+msgstr ""
+
+#: ../pykolab/cli/cmd_acl_cleanup.py:34
+msgid "Clean up ACLs that use identifiers that no longer exist"
+msgstr ""
+
+#: ../pykolab/cli/cmd_acl_cleanup.py:56
+#, python-format
+msgid "Deleting ACL %s for subject %s on folder %s"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:42
+msgid "Specify the (new) alias address"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:45
+msgid "Specify the existing recipient address"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:66 ../pykolab/cli/cmd_add_alias.py:70
+#, python-format
+msgid "Domain %r is not a local domain"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:75
+msgid "Primary and secondary domain do not have the same parent domain"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:81
+#, python-format
+msgid "No such recipient %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:87
+#, python-format
+msgid "Recipient for alias %r already exists"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:97
+msgid "Environment is not configured for "
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_alias.py:105
+#, python-format
+msgid "Recipient %r is not the primary recipient for address %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_domain.py:36
+#: ../pykolab/cli/cmd_count_domain_mailboxes.py:38
+#: ../pykolab/cli/cmd_create_mailbox.py:36
 #: ../pykolab/cli/cmd_export_mailbox.py:33
-#: ../pykolab/cli/cmd_list_mailboxes.py:39
+#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:38
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:36
+#: ../pykolab/cli/cmd_list_mailboxes.py:40
 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:37
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:37
+#: ../pykolab/cli/cmd_list_messages.py:37 ../pykolab/cli/cmd_list_quota.py:36
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:36
+#: ../pykolab/cli/cmd_server_info.py:34
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:38
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:39
+#: ../pykolab/cli/cmd_undelete_mailbox.py:34
 msgid "CLI Options"
-msgstr ""
+msgstr "Kommandozeilenoptionen"
 
 #: ../pykolab/cli/cmd_add_domain.py:42
-msgid "Add domain as alias for DOMAIN"
+msgid "Add alias domain."
 msgstr ""
 
 #: ../pykolab/cli/cmd_add_domain.py:47
-msgid "Add a new domain or domain alias."
+msgid "Add a new domain."
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:55
+#: ../pykolab/cli/cmd_add_domain.py:55 ../pykolab/cli/cmd_delete_domain.py:44
+#: ../pykolab/cli/cmd_find_domain.py:44
 msgid "Could not find credentials with sufficient permissions"
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:80 ../pykolab/wap_client/__init__.py:113
-msgid "Invalid parent domain"
+#: ../pykolab/cli/cmd_add_domain.py:67 ../pykolab/cli/cmd_delete_domain.py:56
+#: ../pykolab/cli/cmd_find_domain.py:56
+msgid "Domain name"
 msgstr ""
 
-#: ../pykolab/cli/cmd_add_domain.py:86
-msgid "Domain name"
+#: ../pykolab/cli/cmd_add_user_subscription.py:37
+msgid "Subscribe a user to a folder."
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_user_subscription.py:47
+#: ../pykolab/cli/cmd_add_user_subscription.py:51
+#: ../pykolab/cli/cmd_remove_user_subscription.py:47
+#: ../pykolab/cli/cmd_remove_user_subscription.py:51
+msgid "Folder pattern"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_user_subscription.py:50
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:63
+#: ../pykolab/cli/cmd_remove_user_subscription.py:50
+msgid "User ID"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_user_subscription.py:72
+#: ../pykolab/cli/cmd_remove_user_subscription.py:72
+#, python-format
+msgid "Cannot subscribe user to folder %r:"
+msgstr ""
+
+#: ../pykolab/cli/cmd_add_user_subscription.py:73
+#: ../pykolab/cli/cmd_delete_message.py:61
+#: ../pykolab/cli/cmd_list_messages.py:67
+#: ../pykolab/cli/cmd_remove_user_subscription.py:73
+msgid "No such folder"
+msgstr ""
+
+#: ../pykolab/cli/cmd_count_domain_mailboxes.py:44
+#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:50
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:48
+#: ../pykolab/cli/cmd_list_mailboxes.py:52 ../pykolab/cli/cmd_list_quota.py:42
+#: ../pykolab/cli/cmd_server_info.py:40
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:57
+msgid "List mailboxes on server SERVER only."
 msgstr ""
 
 #: ../pykolab/cli/cmd_create_mailbox.py:42
 msgid "Set metadata for folder to ANNOTATION=VALUE"
 msgstr ""
 
-#: ../pykolab/cli/cmd_create_mailbox.py:52
-msgid "Invalid argument"
+#: ../pykolab/cli/cmd_create_mailbox.py:50
+msgid "Create folder on PARTITION."
 msgstr ""
 
 #: ../pykolab/cli/cmd_create_mailbox.py:60
+msgid "Invalid argument"
+msgstr ""
+
+#: ../pykolab/cli/cmd_create_mailbox.py:68
 msgid "Invalid argument for metadata"
 msgstr ""
 
+#: ../pykolab/cli/cmd_delete_domain.py:36
+msgid "Delete a domain."
+msgstr ""
+
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:45
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:49
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:50
@@ -684,15 +919,20 @@ msgstr ""
 #: ../pykolab/cli/cmd_list_mailbox_acls.py:43
 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:54
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:65
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:66
+#: ../pykolab/cli/cmd_set_quota.py:46 ../tests/unit/test-015-translate.py:12
+#: ../tests/unit/test-015-translate.py:16
+#: ../tests/unit/test-015-translate.py:18
+#: ../tests/unit/test-015-translate.py:20
 msgid "Folder name"
-msgstr "Ordnername"
+msgstr ""
 
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:60
 #: ../pykolab/cli/cmd_list_mailbox_acls.py:54
 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:80
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:67
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:93
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:94
+#: ../pykolab/cli/cmd_set_quota.py:58
 #, python-format
 msgid "No such folder %r"
 msgstr ""
@@ -701,6 +941,23 @@ msgstr ""
 msgid "No mailbox specified"
 msgstr ""
 
+#: ../pykolab/cli/cmd_delete_mailbox.py:56
+msgid "No such folder(s)"
+msgstr ""
+
+#: ../pykolab/cli/cmd_delete_message.py:36
+msgid "Delete a message from a folder"
+msgstr ""
+
+#: ../pykolab/cli/cmd_delete_message.py:49
+msgid "Specify a UID"
+msgstr ""
+
+#: ../pykolab/cli/cmd_delete_message.py:52
+#: ../pykolab/cli/cmd_list_messages.py:58
+msgid "Specify a folder"
+msgstr ""
+
 #: ../pykolab/cli/cmd_export_mailbox.py:38
 msgid "All folders this user has access to"
 msgstr ""
@@ -720,11 +977,22 @@ msgstr ""
 msgid "No directories found for user %s"
 msgstr ""
 
-#: ../pykolab/cli/cmd_list_mailboxes.py:44
+#: ../pykolab/cli/cmd_find_domain.py:36
+msgid "Find a domain."
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:43
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:41
+#: ../pykolab/cli/cmd_list_mailboxes.py:45
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:41
 msgid "Display raw IMAP UTF-7 folder names"
 msgstr ""
 
-#: ../pykolab/cli/cmd_list_mailboxes.py:75
+#: ../pykolab/cli/cmd_list_domain_mailboxes.py:58
+msgid "Domain"
+msgstr "Domäne"
+
+#: ../pykolab/cli/cmd_list_mailboxes.py:87
 #, python-format
 msgid "Appending folder search for %r"
 msgstr ""
@@ -733,11 +1001,41 @@ msgstr ""
 msgid "List annotations as user USER"
 msgstr ""
 
-#: ../pykolab/cli/cmd_list_quota.py:59 ../pykolab/cli/cmd_list_quota.py:71
+#: ../pykolab/cli/cmd_list_messages.py:43
+msgid "Include messages flagged as \\Deleted"
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_messages.py:47
+msgid "List messages in a folder"
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_quota.py:73 ../pykolab/cli/cmd_list_quota.py:89
 #, python-format
 msgid "The quota for folder %s is set to literally allow 0KB of storage."
 msgstr ""
 
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:47
+msgid "List unsubscribed folders"
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:50
+msgid "List the folders a user is subscribed to."
+msgstr ""
+
+#: ../pykolab/cli/cmd_list_user_subscriptions.py:98
+#, python-format
+msgid "No unsubscribed folders for user %s"
+msgstr ""
+
+#: ../pykolab/cli/cmd_mailbox_cleanup.py:37
+msgid "Clean up mailboxes that do no longer have an owner."
+msgstr ""
+
+#: ../pykolab/cli/cmd_mailbox_cleanup.py:61
+#, python-format
+msgid "Deleting folder 'user/%s'"
+msgstr ""
+
 #: ../pykolab/cli/cmd_remove_mailaddress.py:49
 msgid "Invalid or unqualified email address."
 msgstr ""
@@ -761,20 +1059,34 @@ msgstr ""
 msgid "Found the following recipients:"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:48
+#: ../pykolab/cli/cmd_remove_user_subscription.py:37
+msgid "Unsubscribe a user from a folder."
+msgstr ""
+
+#: ../pykolab/cli/cmd_remove_user_subscription.py:86
+#, python-format
+msgid "Successfully unsubscribed user %s from the following folders:"
+msgstr ""
+
+#: ../pykolab/cli/cmd_remove_user_subscription.py:92
+#, python-format
+msgid "User %s was not unsubscribed from any folders."
+msgstr ""
+
+#: ../pykolab/cli/cmd_rename_mailbox.py:52
 msgid "No target mailbox name specified"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:50
+#: ../pykolab/cli/cmd_rename_mailbox.py:54
 msgid "No source mailbox name specified"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:62
+#: ../pykolab/cli/cmd_rename_mailbox.py:66
 #, python-format
 msgid "Source folder %r does not exist"
 msgstr ""
 
-#: ../pykolab/cli/cmd_rename_mailbox.py:66
+#: ../pykolab/cli/cmd_rename_mailbox.py:70
 #, python-format
 msgid "Target folder %r already exists"
 msgstr ""
@@ -785,21 +1097,75 @@ msgstr ""
 msgid "ACI Permissions"
 msgstr ""
 
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:44
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:45
 msgid "Set annotation as user USER"
 msgstr ""
 
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:58
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:62
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:59
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:63
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:68
 msgid "Metadata value"
 msgstr ""
 
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:61
-#: ../pykolab/cli/cmd_set_mailbox_metadata.py:66
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:62
+#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67
 msgid "Metadata path"
 msgstr ""
 
+#: ../pykolab/cli/cmd_set_quota.py:43 ../pykolab/cli/cmd_set_quota.py:47
+msgid "New quota"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:44
+msgid "Delete mailboxes for recipients that do not appear to exist in LDAP."
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:50
+msgid "Display changes, do not apply them."
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:88
+#, python-format
+msgid "Domains in IMAP not in LDAP: %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:101
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:142
+#, python-format
+msgid ""
+"No recipients for '%s' (would have deleted the mailbox if not for --dry-"
+"run)!"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:106
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:147
+#, python-format
+msgid "Deleting mailbox '%s' because it has no recipients"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:110
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:151
+#, python-format
+msgid "An error occurred removing mailbox %r: %r"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:112
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:153
+#, python-format
+msgid "Not automatically deleting shared folder '%s'"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:114
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:155
+#, python-format
+msgid "No recipients for '%s' (use --delete to delete)!"
+msgstr ""
+
+#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:136
+#, python-format
+msgid "Multiple recipients for '%s'!"
+msgstr ""
+
 #: ../pykolab/cli/cmd_sync.py:41
 #, python-format
 msgid "Found %d domains in %d seconds"
@@ -810,37 +1176,126 @@ msgstr ""
 msgid "Running for domain %s"
 msgstr ""
 
-#: ../pykolab/cli/cmd_sync.py:57
+#: ../pykolab/cli/cmd_sync.py:58
 #, python-format
 msgid "Synchronizing users for %s took %d seconds"
 msgstr ""
 
+#: ../pykolab/cli/cmd_undelete_mailbox.py:39
+msgid "Do not actually execute, but state what would have been executed."
+msgstr ""
+
+#: ../pykolab/cli/cmd_undelete_mailbox.py:42
+msgid "Recover mailboxes previously deleted."
+msgstr ""
+
+#: ../pykolab/cli/cmd_user_info.py:39
+msgid "Email address"
+msgstr ""
+
 #. This is a nested command
 #. This is a nested component
-#: ../pykolab/cli/commands.py:101 ../pykolab/setup/components.py:90
+#: ../pykolab/cli/commands.py:98 ../pykolab/setup/components.py:90
 #, python-format
 msgid "Command Group: %s"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:116 ../pykolab/cli/commands.py:121
+#: ../pykolab/cli/commands.py:113 ../pykolab/cli/commands.py:118
 msgid "No such command."
 msgstr ""
 
-#: ../pykolab/cli/commands.py:171 ../pykolab/setup/components.py:231
+#: ../pykolab/cli/commands.py:168 ../pykolab/setup/components.py:231
 #, python-format
 msgid "Command '%s' already registered"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:196 ../pykolab/setup/components.py:257
-#: ../wallace/modules.py:348
+#: ../pykolab/cli/commands.py:193 ../pykolab/setup/components.py:257
+#: ../wallace/modules.py:369
 #, python-format
 msgid "Alias for %s"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:204 ../pykolab/setup/components.py:265
+#: ../pykolab/cli/commands.py:201 ../pykolab/setup/components.py:265
 msgid "Not yet implemented"
 msgstr ""
 
+#: ../pykolab/cli/sieve/cmd_list.py:43 ../pykolab/cli/sieve/cmd_put.py:42
+#: ../pykolab/cli/sieve/cmd_refresh.py:44 ../pykolab/cli/sieve/cmd_test.py:43
+msgid "Email Address"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:99
+#: ../pykolab/plugins/sievemgmt/__init__.py:111
+#, python-format
+msgid "Found the following scripts for user %s: %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:100
+#: ../pykolab/plugins/sievemgmt/__init__.py:112
+#, python-format
+msgid "And the following script is active for user %s: %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:178
+#: ../pykolab/plugins/sievemgmt/__init__.py:190
+#, python-format
+msgid ""
+"Delivery to folder active, but no folder name attribute available for user "
+"%r"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:181
+#: ../pykolab/plugins/sievemgmt/__init__.py:193
+msgid "Delivery to folder active, but no folder name attribute configured"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:359
+#, python-format
+msgid "MANAGEMENT script for user %s contents: %r"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:364
+#: ../pykolab/plugins/sievemgmt/__init__.py:374
+#, python-format
+msgid "Uploading script MANAGEMENT failed for user %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:366
+#: ../pykolab/plugins/sievemgmt/__init__.py:376
+#, python-format
+msgid "Uploading script MANAGEMENT for user %s succeeded"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:377
+#: ../pykolab/plugins/sievemgmt/__init__.py:387
+#, python-format
+msgid "Including script %s in USER (for user %s)"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:386
+#: ../pykolab/plugins/sievemgmt/__init__.py:396
+#, python-format
+msgid "Uploading script USER failed for user %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:388
+#: ../pykolab/plugins/sievemgmt/__init__.py:398
+#, python-format
+msgid "Uploading script USER for user %s succeeded"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:416
+#: ../pykolab/plugins/sievemgmt/__init__.py:426
+#, python-format
+msgid "Uploading script MASTER failed for user %s"
+msgstr ""
+
+#: ../pykolab/cli/sieve/cmd_refresh.py:418
+#: ../pykolab/plugins/sievemgmt/__init__.py:428
+#, python-format
+msgid "Uploading script MASTER for user %s succeeded"
+msgstr ""
+
 #: ../pykolab/cli/telemetry/cmd_examine_command_issue.py:40
 msgid "Unspecified command issue identifier"
 msgstr ""
@@ -986,357 +1441,429 @@ msgstr ""
 msgid "No command supplied"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:411
+#: ../pykolab/conf/__init__.py:416
 msgid "Insufficient options. Need section, key and value -in that order."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:414
+#: ../pykolab/conf/__init__.py:419
 #, python-format
 msgid "No section '%s' exists."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:445
+#: ../pykolab/conf/__init__.py:461
 #, python-format
 msgid "Setting %s to %r (from the default values for CLI options)"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:514
+#: ../pykolab/conf/__init__.py:534
 #, python-format
 msgid "Could not execute configuration function: %s"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:522
+#: ../pykolab/conf/__init__.py:542
 #, python-format
 msgid "Option %s/%s does not exist in config file %s, pulling from defaults"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:530 ../pykolab/conf/__init__.py:533
+#: ../pykolab/conf/__init__.py:550 ../pykolab/conf/__init__.py:553
 msgid "Option does not exist in defaults."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:543
+#: ../pykolab/conf/__init__.py:563
 #, python-format
 msgid "Configuration file %s not readable."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:546
+#: ../pykolab/conf/__init__.py:566
 #, python-format
 msgid "Configuration file %s does not exist."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:551
+#: ../pykolab/conf/__init__.py:571
 msgid ""
-"WARNING: A negative debug level value does not make this program be any more "
-"silent."
+"WARNING: A negative debug level value does not make this program be any more"
+" silent."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:557
+#: ../pykolab/conf/__init__.py:577
 msgid "This program has 9 levels of verbosity. Using the maximum of 9."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:565 ../pykolab/conf/__init__.py:571
+#: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591
 msgid "Cannot start SASL authentication daemon"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:582
+#: ../pykolab/conf/__init__.py:602
 msgid "No imaplib library found."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:592
+#: ../pykolab/conf/__init__.py:612
 msgid "No LMTP class found in the smtplib library."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:602
+#: ../pykolab/conf/__init__.py:622
 msgid "No SMTP class found in the smtplib library."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:616
+#: ../pykolab/conf/__init__.py:636
 #, python-format
 msgid "Found you specified a specific set of items to test: %s"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:624
+#: ../pykolab/conf/__init__.py:644
 #, python-format
 msgid "Selectively selecting: %s"
 msgstr ""
 
 #: ../pykolab/constants.py.in:40
-msgid "PyKolab is a Kolab Systems product. For more information "
+msgid ""
+"PyKolab is a Kolab Systems product. For more information about Kolab or "
+"PyKolab, visit http://www.kolabsys.com"
 msgstr ""
 
-#: ../pykolab/constants.py.in:54
+#: ../pykolab/constants.py.in:53
 msgid "WARNING"
 msgstr ""
 
-#: ../pykolab/constants.py.in:54
-msgid "The Fully Qualified "
+#: ../pykolab/constants.py.in:53
+msgid ""
+"The Fully Qualified Domain Name or FQDN for this system is incorrect. "
+"Falling back to 'localdomain'."
 msgstr ""
 
-#: ../pykolab/constants.py.in:75
+#: ../pykolab/constants.py.in:72
 msgid "389 Directory Server or Red Hat Directory Server"
 msgstr ""
 
-#: ../pykolab/constants.py.in:79 ../pykolab/constants.py.in:83
+#: ../pykolab/constants.py.in:76 ../pykolab/constants.py.in:80
 msgid "OpenLDAP or compatible"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:79
+#: ../pykolab/imap/cyrus.py:80
 #, python-format
 msgid "Could not connect to Cyrus IMAP server %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:136
+#: ../pykolab/imap/cyrus.py:137
 #, python-format
 msgid "Continuing with separator: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:141
+#: ../pykolab/imap/cyrus.py:142
 msgid "Detected we are running in a Murder topology"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:145
+#: ../pykolab/imap/cyrus.py:146
 msgid "This system is not part of a murder topology"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:166
+#: ../pykolab/imap/cyrus.py:167
 #, python-format
 msgid "Checking actual backend server for folder %s through annotations"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:181
+#: ../pykolab/imap/cyrus.py:172
+msgid "Possibly reproducing the find "
+msgstr ""
+
+#: ../pykolab/imap/cyrus.py:195
 #, python-format
 msgid "Could not get the annotations after %s tries."
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:185
+#: ../pykolab/imap/cyrus.py:199
 #, python-format
 msgid "No annotations for %s: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:192
+#: ../pykolab/imap/cyrus.py:206
 #, python-format
 msgid "Server for INBOX folder %s is %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:204
+#: ../pykolab/imap/cyrus.py:226
 #, python-format
 msgid "Setting quota for folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:208
+#: ../pykolab/imap/cyrus.py:230
 #, python-format
 msgid "Could not set quota for mailfolder %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:217
+#: ../pykolab/imap/cyrus.py:239
 #, python-format
 msgid "Moving INBOX folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:232
+#: ../pykolab/imap/cyrus.py:254
 #, python-format
 msgid "Setting annotation %s on folder %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:237
+#: ../pykolab/imap/cyrus.py:259
 #, python-format
 msgid "Could not set annotation %r on mail folder %r: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:241
+#: ../pykolab/imap/cyrus.py:263
 #, python-format
 msgid "Transferring folder %s from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:301
+#: ../pykolab/imap/cyrus.py:323
 #, python-format
 msgid "Undeleting %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:45
+#: ../pykolab/imap/cyrus.py:334
+#, python-format
+msgid "Would have transfered %s from %s to %s"
+msgstr ""
+
+#: ../pykolab/imap/cyrus.py:336
+#, python-format
+msgid "Would have renamed %s to %s"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:46
 #, python-format
 msgid "Cleaning up ACL entries for %s across all folders"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:60
+#: ../pykolab/imap/__init__.py:61
 #, python-format
 msgid "Cleaning up ACL entries referring to identifier %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:69
+#: ../pykolab/imap/__init__.py:70
 #, python-format
 msgid "Iterating over %d folders"
 msgstr ""
 
 #. Set the ACL to '' (effectively deleting the ACL entry)
-#: ../pykolab/imap/__init__.py:82
+#: ../pykolab/imap/__init__.py:83
 #, python-format
 msgid "Removing acl %r for subject %r from folder %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:143
+#: ../pykolab/imap/__init__.py:145
+msgid "No administrator password is available."
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:153
 #, python-format
 msgid "Logging on to Cyrus IMAP server %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:152
+#: ../pykolab/imap/__init__.py:162
 #, python-format
 msgid "Logging on to Dovecot IMAP server %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:161
+#: ../pykolab/imap/__init__.py:171
 #, python-format
 msgid "Logging on to generic IMAP server %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:179
+#: ../pykolab/imap/__init__.py:189
 #, python-format
 msgid "Reusing existing IMAP server connection to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:181
+#: ../pykolab/imap/__init__.py:191
 #, python-format
 msgid "Reconnecting to IMAP server %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:197
+#: ../pykolab/imap/__init__.py:208
 msgid "Called imap.disconnect() on a server that we had no connection to."
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:212
+#: ../pykolab/imap/__init__.py:222 ../pykolab/imap/__init__.py:234
 #, python-format
-msgid "%r has no attribute %s"
+msgid "Could not create folder %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:223
+#, python-format
+msgid " on server %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:279
-msgid "Private annotations need to be set using the appropriate user account."
+#: ../pykolab/imap/__init__.py:244 ../pykolab/imap/__init__.py:246
+#, python-format
+msgid "%r has no attribute %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:294 ../pykolab/imap/__init__.py:329
+#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428
 #, python-format
 msgid "Creating new shared folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:354 ../pykolab/imap/__init__.py:504
+#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675
 #, python-format
 msgid "Downcasing mailbox name %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:358
+#: ../pykolab/imap/__init__.py:457
 #, python-format
 msgid "Creating new mailbox for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:387
+#: ../pykolab/imap/__init__.py:470
+msgid "Waiting for the Cyrus IMAP Murder to settle..."
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:516
 #, python-format
 msgid "Creating additional folders for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:407
+#: ../pykolab/imap/__init__.py:535
+#, python-format
+msgid "Waiting for the Cyrus murder to settle... %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:547
+#, python-format
+msgid "Correcting additional folder name from %r to %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:553
 #, python-format
 msgid "Mailbox already exists: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:443
+#: ../pykolab/imap/__init__.py:593
 msgid "Subscribing user to the additional folders"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:458
+#: ../pykolab/imap/__init__.py:607
+msgid "Using the following tests for folder subscriptions:"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:609
+#, python-format
+msgid "    %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:612
 #, python-format
 msgid "Folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:470
+#: ../pykolab/imap/__init__.py:624
 #, python-format
 msgid "Subscribing %s to folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:520 ../pykolab/imap/__init__.py:594
+#: ../pykolab/imap/__init__.py:628
+#, python-format
+msgid "Subscribing %s to folder %s failed: %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:658
+#, python-format
+msgid "Could not rename %s to reside on partition %s"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:691
+#, python-format
+msgid "INBOX folder to rename (%s) does not exist"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770
 #, python-format
 msgid "Renaming INBOX from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:524
+#: ../pykolab/imap/__init__.py:698
 #, python-format
 msgid "Could not rename INBOX folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:526 ../pykolab/imap/__init__.py:598
+#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774
 #, python-format
-msgid "Moving INBOX folder %s won't succeed as target folder %s already exists"
+msgid ""
+"Moving INBOX folder %s won't succeed as target folder %s already exists"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:536
+#: ../pykolab/imap/__init__.py:704
+#, python-format
+msgid "Server for mailbox %r is %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:712
 #, python-format
 msgid "Looking for folder '%s', we found folders: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:559
+#: ../pykolab/imap/__init__.py:735
 #, python-format
 msgid "Setting ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:570
+#: ../pykolab/imap/__init__.py:746
 #, python-format
 msgid "Removing ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:591
+#: ../pykolab/imap/__init__.py:767
 #, python-format
 msgid "Found old INBOX folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:600
+#: ../pykolab/imap/__init__.py:776
 #, python-format
 msgid "Did not find old folder user/%s to rename"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:602
+#: ../pykolab/imap/__init__.py:778
 msgid "Value for user is not a dictionary"
 msgstr ""
 
 #. TODO: Go in fact correct the quota.
-#: ../pykolab/imap/__init__.py:662
+#: ../pykolab/imap/__init__.py:846
 #, python-format
 msgid "Cannot get current IMAP quota for folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:675
+#: ../pykolab/imap/__init__.py:859
 #, python-format
 msgid "Quota for %s currently is %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:681
+#: ../pykolab/imap/__init__.py:865
 #, python-format
 msgid "Adjusting authentication database quota for folder %s to %d"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:686
+#: ../pykolab/imap/__init__.py:870
 #, python-format
 msgid "Correcting quota for %s to %s (currently %s)"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:763
+#: ../pykolab/imap/__init__.py:947
 #, python-format
 msgid "Checking folder: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:768
+#: ../pykolab/imap/__init__.py:952
 #, python-format
 msgid "Folder has no corresponding user (1): %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:771
+#: ../pykolab/imap/__init__.py:955
 #, python-format
 msgid "Folder has no corresponding user (2): %s"
 msgstr ""
 
 #. We got user identifier only
-#: ../pykolab/imap/__init__.py:786
+#: ../pykolab/imap/__init__.py:970
 msgid "Please don't give us just a user identifier"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:789
+#: ../pykolab/imap/__init__.py:973
 #, python-format
 msgid "Deleting folder %s"
 msgstr ""
@@ -1345,12 +1872,50 @@ msgstr ""
 msgid "Returning thread local configuration"
 msgstr ""
 
-#: ../pykolab/logger.py:106
+#: ../pykolab/itip/__init__.py:43
+#, python-format
+msgid "Method %r not really interesting for us."
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:49
+#, python-format
+msgid "Raw iTip payload: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:59
+msgid "Could not read iTip from message."
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:67
+#, python-format
+msgid "Duplicate iTip object: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:90
+msgid "iTip event without a start"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:132
+msgid "Message is not an iTip message (non-multipart message)"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:225
+#, python-format
+msgid "Failed to compose iTip reply message: %r"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936
+#: ../wallace/module_resources.py:964
 #, python-format
-msgid "Could not change the ownership of log file %s"
+msgid "SMTP sendmail error: %r"
 msgstr ""
 
-#: ../pykolab/logger.py:122
+#: ../pykolab/logger.py:173 ../pykolab/logger.py:179
+#, python-format
+msgid "Could not change permissions on %s: %r"
+msgstr ""
+
+#: ../pykolab/logger.py:196
 #, python-format
 msgid "Cannot log to file %s: %s"
 msgstr ""
@@ -1448,27 +2013,51 @@ msgstr ""
 msgid "Attribute substitution for 'mail' failed in Recipient Policy"
 msgstr ""
 
-#: ../pykolab/plugins/recipientpolicy/__init__.py:115
+#: ../pykolab/plugins/recipientpolicy/__init__.py:116
 msgid "Could not parse the alternative mail routines"
 msgstr ""
 
-#: ../pykolab/plugins/recipientpolicy/__init__.py:119
+#: ../pykolab/plugins/recipientpolicy/__init__.py:120
 #, python-format
 msgid "Alternative mail routines: %r"
 msgstr ""
 
-#: ../pykolab/plugins/recipientpolicy/__init__.py:130
-#: ../pykolab/plugins/recipientpolicy/__init__.py:141
+#: ../pykolab/plugins/recipientpolicy/__init__.py:127
+#, python-format
+msgid ""
+"An error occurred in composing the secondary mail attribute for entry %r"
+msgstr ""
+
+#: ../pykolab/plugins/recipientpolicy/__init__.py:138
+#: ../pykolab/plugins/recipientpolicy/__init__.py:153
 #, python-format
 msgid "Appending additional mail address: %s"
 msgstr ""
 
-#: ../pykolab/plugins/recipientpolicy/__init__.py:134
-#: ../pykolab/plugins/recipientpolicy/__init__.py:145
+#: ../pykolab/plugins/recipientpolicy/__init__.py:142
+#, python-format
+msgid "Policy for secondary email address failed: %r"
+msgstr ""
+
+#: ../pykolab/plugins/recipientpolicy/__init__.py:157
 msgid ""
 "Attribute substitution for 'alternative_mail' failed in Recipient Policy"
 msgstr ""
 
+#: ../pykolab/plugins/roundcubedb/__init__.py:48
+#, python-format
+msgid "user_delete: %r"
+msgstr ""
+
+#: ../pykolab/plugins/roundcubedb/__init__.py:55
+#: ../pykolab/setup/setup_roundcube.py:160
+msgid "Roundcube installation path not found."
+msgstr ""
+
+#: ../pykolab/plugins/sievemgmt/__init__.py:51
+msgid "Wrong number of arguments for sieve management plugin"
+msgstr ""
+
 #: ../pykolab/setup/components.py:58
 msgid "Display this help."
 msgstr ""
@@ -1489,61 +2078,29 @@ msgstr ""
 msgid "Free/Busy is not installed on this system"
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:55
-msgid ""
-"\n"
-"                        Please supply the MySQL password for the "
-"'roundcube'\n"
-"                        user. You have supplied this password earlier, and "
-"it is\n"
-"                        available from the database URI setting in\n"
-"                        /etc/roundcubemail/db.inc.php.\n"
-"                    "
-msgstr ""
-
-#: ../pykolab/setup/setup_freebusy.py:64
-#: ../pykolab/setup/setup_roundcube.py:56
-msgid "MySQL roundcube password"
+#: ../pykolab/setup/setup_imap.py:45
+msgid "Setup IMAP."
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:92
-#: ../pykolab/setup/setup_roundcube.py:115 ../pykolab/setup/setup_zpush.py:71
-#, python-format
-msgid "Using template file %r"
+#: ../pykolab/setup/setup_imap.py:89
+msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:99
-#: ../pykolab/setup/setup_roundcube.py:122 ../pykolab/setup/setup_zpush.py:78
-#, python-format
-msgid "Successfully compiled template %r, writing out to %r"
+#: ../pykolab/setup/setup_imap.py:114
+msgid "Could not write out Cyrus IMAP configuration file /etc/cyrus.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:119
-#: ../pykolab/setup/setup_roundcube.py:193
-#: ../pykolab/setup/setup_syncroton.py:66 ../pykolab/setup/setup_zpush.py:98
-msgid "Could not start the webserver server service."
+#: ../pykolab/setup/setup_imap.py:158
+msgid "Could not start the cyrus-imapd and kolab-saslauthd services."
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:128 ../pykolab/setup/setup_imap.py:169
-#: ../pykolab/setup/setup_kolabd.py:81 ../pykolab/setup/setup_ldap.py:327
-#: ../pykolab/setup/setup_mta.py:378 ../pykolab/setup/setup_mysql.py:58
-#: ../pykolab/setup/setup_roundcube.py:202
-#: ../pykolab/setup/setup_syncroton.py:75 ../pykolab/setup/setup_zpush.py:107
+#: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:81
+#: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:455
+#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:237
+#: ../pykolab/setup/setup_syncroton.py:102
 msgid "Could not configure to start on boot, the "
 msgstr ""
 
-#: ../pykolab/setup/setup_imap.py:45
-msgid "Setup IMAP."
-msgstr ""
-
-#: ../pykolab/setup/setup_imap.py:89 ../pykolab/setup/setup_imap.py:114
-msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf"
-msgstr ""
-
-#: ../pykolab/setup/setup_imap.py:154
-msgid "Could not start the cyrus-imapd and kolab-saslauthd services."
-msgstr ""
-
 #: ../pykolab/setup/setup_kolabd.py:43
 msgid "Setup the Kolab daemon."
 msgstr ""
@@ -1552,8 +2109,7 @@ msgstr ""
 #, python-format
 msgid ""
 "\n"
-"                            Copying the configuration section for 'example."
-"org' over to\n"
+"                            Copying the configuration section for 'example.org' over to\n"
 "                            a section applicable to your domain '%s'.\n"
 "                        "
 msgstr ""
@@ -1574,220 +2130,250 @@ msgstr ""
 msgid "Allow anonymous binds (default: no)."
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:64
-msgid "Setup LDAP."
+#: ../pykolab/setup/setup_ldap.py:68
+msgid "Skip setting up the LDAP server."
 msgstr ""
 
 #: ../pykolab/setup/setup_ldap.py:76
+msgid "Setup configuration for OpenLDAP compatibility."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:84
+msgid "Setup configuration for Active Directory compatibility."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:88
+msgid "Setup LDAP."
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:97
+msgid "Skipping setup of LDAP, as specified"
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:126
+msgid ""
+"\n"
+"                        You can not configure Kolab to run against OpenLDAP\n"
+"                        and Active Directory simultaneously.\n"
+"                    "
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:139
+msgid ""
+"\n"
+"                                It seems 389 Directory Server has an existing\n"
+"                                instance configured. This setup script does not\n"
+"                                intend to destroy or overwrite your data. Please\n"
+"                                make sure /etc/dirsrv/ and /var/lib/dirsrv/ are\n"
+"                                clean so that this setup does not have to worry.\n"
+"                            "
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:154
 msgid ""
 "\n"
-"                        Please supply a password for the LDAP administrator "
-"user\n"
-"                        'admin', used to login to the graphical console of "
-"389\n"
+"                        Please supply a password for the LDAP administrator user\n"
+"                        'admin', used to login to the graphical console of 389\n"
 "                        Directory server.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:84
+#: ../pykolab/setup/setup_ldap.py:162
 msgid "Administrator password"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:91
+#: ../pykolab/setup/setup_ldap.py:169
 msgid ""
 "\n"
-"                        Please supply a password for the LDAP Directory "
-"Manager\n"
-"                        user, which is the administrator user you will be "
-"using\n"
-"                        to at least initially log in to the Web Admin, and "
-"that\n"
+"                        Please supply a password for the LDAP Directory Manager\n"
+"                        user, which is the administrator user you will be using\n"
+"                        to at least initially log in to the Web Admin, and that\n"
 "                        Kolab uses to perform administrative tasks.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:100
+#: ../pykolab/setup/setup_ldap.py:178
 msgid "Directory Manager password"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:107
+#: ../pykolab/setup/setup_ldap.py:185
 msgid ""
 "\n"
 "                        Please choose the system user and group the service\n"
 "                        should use to run under. These should be existing,\n"
-"                        unprivileged, local system POSIX accounts with no "
-"shell.\n"
+"                        unprivileged, local system POSIX accounts with no shell.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:117 ../pykolab/setup/setup_ldap.py:120
+#: ../pykolab/setup/setup_ldap.py:195 ../pykolab/setup/setup_ldap.py:198
 msgid "User"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:118 ../pykolab/setup/setup_ldap.py:121
+#: ../pykolab/setup/setup_ldap.py:196 ../pykolab/setup/setup_ldap.py:199
 msgid "Group"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:157
+#: ../pykolab/setup/setup_ldap.py:234
 msgid ""
 "\n"
-"                        This setup procedure plans to set up Kolab Groupware "
-"for\n"
-"                        the following domain name space. This domain name "
-"is\n"
+"                        This setup procedure plans to set up Kolab Groupware for\n"
+"                        the following domain name space. This domain name is\n"
 "                        obtained from the reverse DNS entry on your network\n"
-"                        interface. Please confirm this is the appropriate "
-"domain\n"
+"                        interface. Please confirm this is the appropriate domain\n"
 "                        name space.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:171
+#: ../pykolab/setup/setup_ldap.py:248
 msgid "Domain name to use"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:176 ../pykolab/setup/setup_ldap.py:201
+#: ../pykolab/setup/setup_ldap.py:253 ../pykolab/setup/setup_ldap.py:278
 msgid ""
 "\n"
 "                                    Invalid input. Please try again.\n"
 "                                "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:185
+#: ../pykolab/setup/setup_ldap.py:262
 msgid ""
 "\n"
-"                        The standard root dn we composed for you follows. "
-"Please\n"
+"                        The standard root dn we composed for you follows. Please\n"
 "                        confirm this is the root dn you wish to use.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:196
+#: ../pykolab/setup/setup_ldap.py:273
 msgid "Root DN to use"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:244
+#: ../pykolab/setup/setup_ldap.py:325
 msgid "No directory server setup tool available."
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:255
+#: ../pykolab/setup/setup_ldap.py:337
 msgid ""
 "\n"
-"                    Setup is now going to set up the 389 Directory Server. "
-"This\n"
-"                    may take a little while (during which period there is "
-"no\n"
+"                    Setup is now going to set up the 389 Directory Server. This\n"
+"                    may take a little while (during which period there is no\n"
 "                    output and no progress indication).\n"
 "                "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:262
+#: ../pykolab/setup/setup_ldap.py:344
 msgid "Setting up 389 Directory Server"
 msgstr ""
 
-#. TODO: Get the return code and display output if not successful.
-#: ../pykolab/setup/setup_ldap.py:274
+#: ../pykolab/setup/setup_ldap.py:356
+msgid ""
+"\n"
+"                        An error was detected in the setup procedure for 389\n"
+"                        Directory Server. This setup will write out stderr and\n"
+"                        stdout to /var/log/kolab/setup.error.log and\n"
+"                        /var/log/kolab/setup.out.log respectively, before it\n"
+"                        exits.\n"
+"                    "
+msgstr ""
+
+#: ../pykolab/setup/setup_ldap.py:373
 msgid "Setup DS stdout:"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:277
+#: ../pykolab/setup/setup_ldap.py:376
 msgid "Setup DS stderr:"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:303
+#: ../pykolab/setup/setup_ldap.py:402
 msgid "Could not copy the LDAP extensions for Kolab"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:306
+#: ../pykolab/setup/setup_ldap.py:405
 msgid "Could not find the ldap Kolab schema file"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:318
+#: ../pykolab/setup/setup_ldap.py:417
 msgid "Could not start the directory server service."
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:332
+#: ../pykolab/setup/setup_ldap.py:431
 msgid ""
 "\n"
 "                        Please supply a Cyrus Administrator password. This\n"
 "                        password is used by Kolab to execute administrative\n"
 "                        tasks in Cyrus IMAP. You may also need the password\n"
 "                        yourself to troubleshoot Cyrus IMAP and/or perform\n"
-"                        other administrative tasks against Cyrus IMAP "
-"directly.\n"
+"                        other administrative tasks against Cyrus IMAP directly.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:342
+#: ../pykolab/setup/setup_ldap.py:441
 msgid "Cyrus Administrator password"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:349
+#: ../pykolab/setup/setup_ldap.py:448
 msgid ""
 "\n"
-"                        Please supply a Kolab Service account password. "
-"This\n"
-"                        account is used by various services such as "
-"Postfix,\n"
-"                        and Roundcube, as anonymous binds to the LDAP "
-"server\n"
+"                        Please supply a Kolab Service account password. This\n"
+"                        account is used by various services such as Postfix,\n"
+"                        and Roundcube, as anonymous binds to the LDAP server\n"
 "                        will not be allowed.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:358
+#: ../pykolab/setup/setup_ldap.py:457
 msgid "Kolab Service password"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:368
+#: ../pykolab/setup/setup_ldap.py:467
 msgid "Writing out configuration to kolab.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:382
+#: ../pykolab/setup/setup_ldap.py:481
 msgid "Inserting service users into LDAP."
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:456
+#: ../pykolab/setup/setup_ldap.py:555
 msgid "Writing out cn=kolab,cn=config"
 msgstr ""
 
 #. TODO: Add kolab-admin role
 #. TODO: Assign kolab-admin admin ACLs
-#: ../pykolab/setup/setup_ldap.py:480
+#: ../pykolab/setup/setup_ldap.py:579
 #, python-format
 msgid "Adding domain %s to list of domains for this deployment"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:497
+#: ../pykolab/setup/setup_ldap.py:607
 msgid "Disabling anonymous binds"
 msgstr ""
 
 #. TODO: Ensure the uid attribute is unique
-#. TODO^2: Consider renaming the general "attribute uniqueness to "uid attribute uniqueness"
-#: ../pykolab/setup/setup_ldap.py:505
+#. TODO^2: Consider renaming the general "attribute uniqueness to "uid
+#. attribute uniqueness"
+#: ../pykolab/setup/setup_ldap.py:615
 msgid "Enabling attribute uniqueness plugin"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:511
+#: ../pykolab/setup/setup_ldap.py:621
 msgid "Enabling referential integrity plugin"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:517
+#: ../pykolab/setup/setup_ldap.py:627
 msgid "Enabling and configuring account policy plugin"
 msgstr ""
 
 #. TODO: Add kolab-admin role
-#: ../pykolab/setup/setup_ldap.py:532
+#: ../pykolab/setup/setup_ldap.py:642
 msgid "Adding the kolab-admin role"
 msgstr ""
 
 #. TODO: User writeable attributes on root_dn
-#: ../pykolab/setup/setup_ldap.py:543
+#: ../pykolab/setup/setup_ldap.py:653
 #, python-format
 msgid "Setting access control to %s"
 msgstr ""
 
-#: ../pykolab/setup/setup_ldap.py:568
+#: ../pykolab/setup/setup_ldap.py:679
 msgid "Could not start and configure to start on boot, the "
 msgstr ""
 
@@ -1795,24 +2381,24 @@ msgstr ""
 msgid "Setup MTA."
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:245 ../pykolab/setup/setup_php.py:104
+#: ../pykolab/setup/setup_mta.py:317 ../pykolab/setup/setup_php.py:106
 #, python-format
 msgid "Setting key %r to %r"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:278
+#: ../pykolab/setup/setup_mta.py:350
 msgid "Could not write out Postfix configuration file /etc/postfix/master.cf"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:321
-msgid "Could not write out Amavis configuration file /etc/amavisd/amavisd.conf"
+#: ../pykolab/setup/setup_mta.py:397
+msgid "Could not write out Amavis configuration file amavisd.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:329
+#: ../pykolab/setup/setup_mta.py:405
 msgid "Not writing out any configuration for Amavis."
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:360
+#: ../pykolab/setup/setup_mta.py:437
 msgid "Could not start the postfix, clamav and amavisd services services."
 msgstr ""
 
@@ -1824,55 +2410,50 @@ msgstr ""
 msgid "Could not start the MySQL database service."
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:68
+#: ../pykolab/setup/setup_mysql.py:71
 msgid "What MySQL server are we setting up?"
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:72
+#: ../pykolab/setup/setup_mysql.py:75
 msgid ""
 "\n"
-"                        Please supply the root password for MySQL, so we can "
-"set\n"
-"                        up user accounts for other components that use "
-"MySQL.\n"
+"                        Please supply the root password for MySQL, so we can set\n"
+"                        up user accounts for other components that use MySQL.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:79 ../pykolab/setup/setup_mysql.py:96
+#: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99
+#: ../pykolab/setup/setup_roundcube.py:183
+#: ../pykolab/setup/setup_syncroton.py:63
 msgid "MySQL root password"
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:85
+#: ../pykolab/setup/setup_mysql.py:88
 msgid ""
 "\n"
-"                        Please supply a root password for MySQL. This "
-"password\n"
-"                        will be the administrative user for this MySQL "
-"server,\n"
-"                        and it should be kept a secret. After this setup "
-"process\n"
+"                        Please supply a root password for MySQL. This password\n"
+"                        will be the administrative user for this MySQL server,\n"
+"                        and it should be kept a secret. After this setup process\n"
 "                        has completed, Kolab is going to discard and forget\n"
 "                        about this password, but you will need it for\n"
 "                        administrative tasks in MySQL.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:136
+#: ../pykolab/setup/setup_mysql.py:139
 msgid ""
 "\n"
-"                        Please supply a password for the MySQL user "
-"'kolab'.\n"
-"                        This password will be used by Kolab services, such "
-"as\n"
+"                        Please supply a password for the MySQL user 'kolab'.\n"
+"                        This password will be used by Kolab services, such as\n"
 "                        the Web Administration Panel.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:144
+#: ../pykolab/setup/setup_mysql.py:147
 msgid "MySQL kolab password"
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:162
+#: ../pykolab/setup/setup_mysql.py:165
 msgid "Could not find the MySQL Kolab schema file"
 msgstr ""
 
@@ -1896,19 +2477,21 @@ msgstr ""
 msgid ""
 "\n"
 "                        Please supply the timezone PHP should be using.\n"
+"                        You have to use a Continent or Country / City locality name\n"
+"                        like 'Europe/Berlin', but not just 'CEST'.\n"
 "                    "
 msgstr ""
 
-#: ../pykolab/setup/setup_php.py:72
+#: ../pykolab/setup/setup_php.py:74
 msgid "Timezone ID"
 msgstr ""
 
-#: ../pykolab/setup/setup_php.py:78
+#: ../pykolab/setup/setup_php.py:80
 #, python-format
 msgid "Cannot configure PHP through %r (No such file or directory)"
 msgstr ""
 
-#: ../pykolab/setup/setup_php.py:89
+#: ../pykolab/setup/setup_php.py:91
 msgid "Could not find PHP configuration file php.ini"
 msgstr ""
 
@@ -1919,23 +2502,33 @@ msgstr ""
 #: ../pykolab/setup/setup_roundcube.py:48
 msgid ""
 "\n"
-"                    Please supply a password for the MySQL user "
-"'roundcube'.\n"
+"                    Please supply a password for the MySQL user 'roundcube'.\n"
 "                    This password will be used by the Roundcube webmail\n"
 "                    interface.\n"
 "                "
 msgstr ""
 
-#: ../pykolab/setup/setup_syncroton.py:40
-msgid "Setup Syncroton."
+#: ../pykolab/setup/setup_roundcube.py:56
+msgid "MySQL roundcube password"
 msgstr ""
 
-#: ../pykolab/setup/setup_zpush.py:41
-msgid "Setup zpush."
+#: ../pykolab/setup/setup_roundcube.py:120
+#, python-format
+msgid "Using template file %r"
 msgstr ""
 
-#: ../pykolab/setup/setup_zpush.py:45
-msgid "Z-Push is not installed on this system"
+#: ../pykolab/setup/setup_roundcube.py:127
+#, python-format
+msgid "Successfully compiled template %r, writing out to %r"
+msgstr ""
+
+#: ../pykolab/setup/setup_roundcube.py:228
+#: ../pykolab/setup/setup_syncroton.py:93
+msgid "Could not start the webserver server service."
+msgstr ""
+
+#: ../pykolab/setup/setup_syncroton.py:40
+msgid "Setup Syncroton."
 msgstr ""
 
 #. start_max = (int)(time.time())
@@ -1963,198 +2556,616 @@ msgstr ""
 msgid "No database available"
 msgstr ""
 
-#: ../pykolab/utils.py:60 ../pykolab/utils.py:62
+#: ../pykolab/utils.py:62 ../pykolab/utils.py:64
 #, python-format
 msgid "Confirm %s: "
 msgstr ""
 
-#: ../pykolab/utils.py:65
+#: ../pykolab/utils.py:67
 msgid "Incorrect confirmation. "
 msgstr ""
 
-#: ../pykolab/utils.py:70 ../pykolab/utils.py:75
+#: ../pykolab/utils.py:72 ../pykolab/utils.py:77
 #, python-format
 msgid "%s: "
 msgstr ""
 
-#: ../pykolab/utils.py:72 ../pykolab/utils.py:77
+#: ../pykolab/utils.py:74 ../pykolab/utils.py:79
 #, python-format
 msgid "%s [%s]: "
 msgstr ""
 
-#: ../pykolab/utils.py:122
+#: ../pykolab/utils.py:124
 msgid "Please answer 'yes' or 'no'."
 msgstr ""
 
-#: ../pykolab/utils.py:148
+#: ../pykolab/utils.py:164
 msgid "Choice"
 msgstr ""
 
-#: ../pykolab/utils.py:214
+#: ../pykolab/utils.py:167
+msgid "Choice (type '?' for options)"
+msgstr ""
+
+#: ../pykolab/utils.py:268
 #, python-format
 msgid "Could not change the permissions on %s"
 msgstr ""
 
-#: ../pykolab/utils.py:395
+#: ../pykolab/utils.py:479
 #, python-format
 msgid "Transliterating string %r with locale %r"
 msgstr ""
 
-#: ../pykolab/utils.py:403
+#: ../pykolab/utils.py:487
 msgid "Attempting to set locale"
 msgstr ""
 
-#: ../pykolab/utils.py:405
+#: ../pykolab/utils.py:489
 msgid "Success setting locale"
 msgstr ""
 
-#: ../pykolab/utils.py:407
+#: ../pykolab/utils.py:491
 msgid "Failure to set locale"
 msgstr ""
 
-#: ../pykolab/utils.py:415
+#: ../pykolab/utils.py:499
 #, python-format
 msgid "Executing '%s | %s'"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:257
+#: ../pykolab/utils.py:510
+#, python-format
+msgid "Could not translate %s using locale %s"
+msgstr ""
+
+#: ../pykolab/wap_client/__init__.py:320
 #, python-format
 msgid "Requesting %r with params %r"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:263
+#: ../pykolab/wap_client/__init__.py:328
 #, python-format
 msgid "Got response: %r"
 msgstr ""
 
 #. Some data is not JSON
-#: ../pykolab/wap_client/__init__.py:268
+#: ../pykolab/wap_client/__init__.py:334
 msgid "Response data is not JSON"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:79 ../pykolab/xml/attendee.py:99
+#. support integer values, too
+#: ../pykolab/xml/attendee.py:9 ../pykolab/xml/attendee.py:17
+msgid "Needs Action"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:10 ../pykolab/xml/attendee.py:18
+msgid "Accepted"
+msgstr "Akzeptiert"
+
+#: ../pykolab/xml/attendee.py:11 ../pykolab/xml/attendee.py:19
+msgid "Declined"
+msgstr "Abgelehnt"
+
+#: ../pykolab/xml/attendee.py:12 ../pykolab/xml/attendee.py:20
+msgid "Tentatively Accepted"
+msgstr "Provisorisch Akzeptiert"
+
+#: ../pykolab/xml/attendee.py:13 ../pykolab/xml/attendee.py:21
+msgid "Delegated"
+msgstr "Delegiert"
+
+#: ../pykolab/xml/attendee.py:14
+msgid "Completed"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:15
+msgid "In Process"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130
 msgid "Not a valid attendee"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:84
+#: ../pykolab/xml/attendee.py:115
 msgid "No valid delegator references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:104
+#: ../pykolab/xml/attendee.py:135
 msgid "No valid delegatee references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:140
+#: ../pykolab/xml/attendee.py:180
 #, python-format
 msgid "Invalid cutype %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:151
+#: ../pykolab/xml/attendee.py:192
 #, python-format
 msgid "Invalid participant status %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:159
+#: ../pykolab/xml/attendee.py:200
 #, python-format
 msgid "Invalid role %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:172
+#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708
+#: ../pykolab/xml/event.py:751
+msgid "Event start needs datetime.date or datetime.datetime instance"
+msgstr ""
+
+#: ../pykolab/xml/event.py:241
 #, python-format
 msgid "No attendee with email or name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:180
+#: ../pykolab/xml/event.py:249
 #, python-format
 msgid "Invalid argument value attendee %r, must be basestring or Attendee"
 msgstr ""
 
-#: ../pykolab/xml/event.py:186
+#: ../pykolab/xml/event.py:255
 #, python-format
 msgid "No attendee with email %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:192
+#: ../pykolab/xml/event.py:261
 #, python-format
 msgid "No attendee with name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:338
+#: ../pykolab/xml/event.py:426
 msgid "Invalid participant status"
 msgstr ""
 
-#: ../pykolab/xml/event.py:538
-msgid "Event end needs datetime.date or datetime.datetime instance"
+#: ../pykolab/xml/event.py:542
+#, python-format
+msgid "Invalid status %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:657
+#: ../pykolab/xml/event.py:550
 #, python-format
-msgid "Invalid status %r"
+msgid "Invalid classification %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:678 ../pykolab/xml/event.py:728
-msgid "Event start needs datetime.date or datetime.datetime instance"
+#: ../pykolab/xml/event.py:577
+msgid "Event end needs datetime.date or datetime.datetime instance"
 msgstr ""
 
-#: ../pykolab/xml/event.py:764
+#: ../pykolab/xml/event.py:761
 #, python-format
 msgid "Invalid status set: %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:882
+#: ../pykolab/xml/event.py:923
 msgid "No sender specified"
 msgstr ""
 
-#: ../saslauthd/__init__.py:126 ../saslauthd/__init__.py:134
-msgid "Traceback occurred, please report a bug at http://bugzilla.kolabsys.com"
+#: ../pykolab/xml/event.py:932
+#, python-format
+msgid "Invitation for %s was %s"
+msgstr ""
+
+#: ../pykolab/xml/event.py:937
+msgid "This is an automated response to one of your event requests."
 msgstr ""
 
-#: ../wallace/__init__.py:62
+#: ../saslauthd/__init__.py:99
+#, python-format
+msgid "Could not create %r: %r"
+msgstr ""
+
+#: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145
+#: ../wallace/__init__.py:403 ../wallace/__init__.py:412
+msgid ""
+"Traceback occurred, please report a bug at http://bugzilla.kolabsys.com"
+msgstr ""
+
+#: ../saslauthd/__init__.py:185
+msgid "kolab-saslauthd could not accept "
+msgstr ""
+
+#: ../saslauthd/__init__.py:190
+msgid "Maximum tries exceeded, exiting"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190
+#: ../wallace/module_resources.py:879
+#, python-format
+msgid "Reservation Request for %(summary)s was %(status)s"
+msgstr ""
+
+#. check notification message sent to resource owner (jane)
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:605
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:621
+#: ../wallace/module_resources.py:954
+#, python-format
+msgid "Booking for %s has been %s"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:146
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:720
+#: ../wallace/module_invitationpolicy.py:374
+#, python-format
+msgid "\"%(summary)s\" has been %(status)s"
+msgstr ""
+
+#. check for notification message
+#. this notification should be suppressed until mark has replied, too
+#. this triggers an additional notification
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:616
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:622
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:635
+#: ../wallace/module_invitationpolicy.py:925
+#, python-format
+msgid "\"%s\" has been updated"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:627
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639
+msgid "PENDING"
+msgstr ""
+
+#: ../wallace/__init__.py:57
+#, python-format
+msgid "Wallace modules: %r"
+msgstr ""
+
+#: ../wallace/__init__.py:69
+#, python-format
+msgid "Module %s.execute() failed on message %r with error: %s"
+msgstr ""
+
+#: ../wallace/__init__.py:78
 #, python-format
 msgid "Worker process %s initializing"
 msgstr ""
 
-#: ../wallace/__init__.py:81
+#: ../wallace/__init__.py:100
 msgid "Bind address for Wallace."
 msgstr ""
 
-#: ../wallace/__init__.py:107
+#: ../wallace/__init__.py:126
 msgid "Port that Wallace is supposed to use."
 msgstr ""
 
-#: ../wallace/__init__.py:158
+#: ../wallace/__init__.py:177
 #, python-format
 msgid "Could not bind to socket on port %d on bind "
 msgstr ""
 
-#: ../wallace/__init__.py:170
+#: ../wallace/__init__.py:189
 msgid "Could not shut down socket"
 msgstr ""
 
-#: ../wallace/__init__.py:227
+#: ../wallace/__init__.py:253
 msgid "Accepted connection"
 msgstr ""
 
-#: ../wallace/__init__.py:398
+#: ../wallace/__init__.py:428
 #, python-format
 msgid "Could not write pid file %s"
 msgstr ""
 
-#: ../wallace/module_optout.py:61 ../wallace/module_resources.py:93
+#: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60
+#: ../wallace/module_invitationpolicy.py:168 ../wallace/module_optout.py:61
+#: ../wallace/module_resources.py:120
 #, python-format
 msgid "Issuing callback after processing to stage %s"
 msgstr ""
 
-#: ../wallace/module_optout.py:62 ../wallace/module_resources.py:99
+#: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61
+#: ../wallace/module_invitationpolicy.py:170 ../wallace/module_optout.py:62
+#: ../wallace/module_resources.py:126
 #, python-format
 msgid "Testing cb_action_%s()"
 msgstr ""
 
-#: ../wallace/module_optout.py:64 ../wallace/module_resources.py:102
+#: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63
+#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:64
+#: ../wallace/module_resources.py:129
 #, python-format
 msgid "Attempting to execute cb_action_%s()"
 msgstr ""
 
+#: ../wallace/module_footer.py:67
+#, python-format
+msgid "Executing module footer for %r, %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:66
+#, python-format
+msgid "Executing module gpgencrypt for %r, %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:98
+msgid "Message is already encrypted (app/pgp-enc content-type)"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:102
+msgid "Message already encrypted by main content-type header"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:131
+msgid ""
+"Configured to encrypt to a key not configured, and strict policy enabled. "
+"Bailing out."
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:134
+msgid ""
+"Configured to encrypt to a key not configured, but continuing anyway (see "
+"'gpgencrypt_strict')."
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:171
+#, python-format
+msgid "Recipients: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:183
+#, python-format
+msgid "Current keys: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:188
+#, python-format
+msgid "Retrieving key for recipient: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:192 ../wallace/module_gpgencrypt.py:208
+#, python-format
+msgid "Found matching address %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:200
+#, python-format
+msgid "Found matching address %r in remote keys"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:232
+#, python-format
+msgid "An error occurred: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:154
+#, python-format
+msgid "Invitation policy called for %r, %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:211
+#: ../wallace/module_resources.py:169
+#, python-format
+msgid "Failed to parse iTip events from message: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:215
+msgid ""
+"Message is not an iTip message or does not contain any (valid) iTip events."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:219
+#, python-format
+msgid ""
+"iTip events attached to this message contain the following information: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:232
+#, python-format
+msgid "No itips, no users, pass along %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:235
+#, python-format
+msgid "iTips, but no users, pass along %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:255
+#, python-format
+msgid "No user attendee matching envelope recipient %s, skip message"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:259
+#, python-format
+msgid "Receiving user: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:284
+#, python-format
+msgid "Apply invitation policy %r for domain %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:295
+#, python-format
+msgid "Ignoring '%s' iTip method"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:299
+#, python-format
+msgid "iTip message %r consumed by the invitationpolicy module"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:315
+msgid "Pass invitation for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:320
+#, python-format
+msgid "Receiving Attendee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:339
+#, python-format
+msgid "Existing event: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:350
+#, python-format
+msgid "Precondition for event %r fulfilled: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:386
+#, python-format
+msgid "No RSVP for recipient %r requested"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:412
+msgid "Pass reply for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:419
+#, python-format
+msgid "Sender Attendee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:431
+#, python-format
+msgid ""
+"The iTip reply sequence (%r) doesn't match the referred event version (%r). "
+"Forwarding to Inbox."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:437
+#, python-format
+msgid "Auto-updating event %r on iTip REPLY"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:459
+#: ../wallace/module_invitationpolicy.py:488
+msgid ""
+"The event referred by this reply was not found in the user's calendars. "
+"Forwarding to Inbox."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:472
+msgid "Pass cancellation for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:517
+#, python-format
+msgid "Checking if email address %r belongs to a local user"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:522
+#, python-format
+msgid "User DN: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:524
+#, python-format
+msgid "No user record(s) found for %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:577
+#, python-format
+msgid "User record doesn't have the mailbox attribute %r set"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:590
+#, python-format
+msgid "IMAP proxy authentication failed: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:612
+#, python-format
+msgid "List calendar folders for user %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:628
+#, python-format
+msgid "IMAP metadata for %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:658
+#, python-format
+msgid "Searching folder %r for event %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:670
+#: ../wallace/module_invitationpolicy.py:709
+#: ../wallace/module_resources.py:486
+#, python-format
+msgid "Failed to parse event from message %s/%s: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:696
+#, python-format
+msgid "Listing events from folder %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:715
+#, python-format
+msgid "Existing event %r conflicts with invitation %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:722
+#: ../wallace/module_resources.py:344
+#, python-format
+msgid "start: %r, end: %r, total: %r, messages: %d"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:748
+#, python-format
+msgid "%r is locked, waiting..."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:811
+#, python-format
+msgid "Failed to save event: no calendar folder found for user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:814
+#, python-format
+msgid "Save event %r to user calendar %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:827
+#, python-format
+msgid "Failed to save event to user calendar at %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:843
+#, python-format
+msgid "Delete event %r in %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:863
+#, python-format
+msgid "Compose participation status summary for event %r to user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:901
+#, python-format
+msgid ""
+"Waiting for more automated replies (got %d of %d); skipping notification"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:998
+#, python-format
+msgid "Updated %s's copy of %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1001
+#, python-format
+msgid "Attendee %s's copy of %r not found"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1004
+#, python-format
+msgid "Attendee %r not found in LDAP"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1008
+#, python-format
+msgid ""
+"\n"
+"        %(name)s has %(status)s your invitation for %(summary)s.\n"
+"\n"
+"        *** This is an automated response sent by the Kolab Invitation system ***\n"
+"    "
+msgstr ""
+
 #. modules.next_module('optout')
 #: ../wallace/module_optout.py:70
 #, python-format
@@ -2176,171 +3187,256 @@ msgstr ""
 msgid "Could not send request to optout_url %s"
 msgstr ""
 
-#: ../wallace/module_resources.py:80
+#: ../wallace/module_resources.py:110
 #, python-format
 msgid "Resource Management called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:143
+#: ../wallace/module_resources.py:174
 msgid "Message is not an iTip message or does not contain any "
 msgstr ""
 
-#: ../wallace/module_resources.py:151
+#: ../wallace/module_resources.py:182
 msgid "iTip events attached to this message contain the "
 msgstr ""
 
-#: ../wallace/module_resources.py:171
+#: ../wallace/module_resources.py:205
 msgid "Not an iTip message, but sent to resource nonetheless. Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:179
-msgid "No itips, no resources, pass along"
+#: ../wallace/module_resources.py:213
+#, python-format
+msgid "No itips, no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:183
-msgid "iTips, but no resources, pass along"
+#: ../wallace/module_resources.py:216
+#, python-format
+msgid "iTips, but no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:215
+#: ../wallace/module_resources.py:225
 #, python-format
-msgid "Resources: %r"
+msgid "No resource attendees matching envelope recipient %s, Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:233
+#: ../wallace/module_resources.py:234
 #, python-format
-msgid "Checking events in resource folder %r"
+msgid "Resources: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:240
+#: ../wallace/module_resources.py:244
 #, python-format
-msgid "Mailbox for resource %r doesn't exist"
+msgid "Receiving Resource: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:253
+#: ../wallace/module_resources.py:252
 #, python-format
-msgid "Fetching message UID %r from folder %r"
+msgid "Recipient %r is non-participant, ignoring message"
 msgstr ""
 
-#: ../wallace/module_resources.py:292
+#: ../wallace/module_resources.py:279
 #, python-format
-msgid "Event %r conflicts with event "
+msgid "Accept invitation for individual resource %r / %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:305
+#: ../wallace/module_resources.py:308
 #, python-format
-msgid "start: %r, end: %r, total: %r, messages: %r"
+msgid "Delegate invitation for resource collection %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:312
+#: ../wallace/module_resources.py:340
+#, python-format
+msgid "Failed to read resource calendar for %r: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:350
 #, python-format
 msgid "Polling for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:316
+#: ../wallace/module_resources.py:353
 #, python-format
 msgid "Resource %r has been popped from the list"
 msgstr ""
 
-#: ../wallace/module_resources.py:323
+#: ../wallace/module_resources.py:357
 msgid "Resource is a collection"
 msgstr ""
 
-#: ../wallace/module_resources.py:371 ../wallace/module_resources.py:421
+#: ../wallace/module_resources.py:368
 #, python-format
-msgid "Adding event to %r"
+msgid "Removed conflicting resources from %r: (%r) => %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:470
+#: ../wallace/module_resources.py:380
 #, python-format
-msgid "Method %r not really interesting for us."
+msgid "Conflicting events: %r for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:478
+#: ../wallace/module_resources.py:397
 #, python-format
-msgid "Raw iTip payload: %s"
+msgid "Delegate to another resource collection member: %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:488
-msgid "Could not read iTip from message."
+#: ../wallace/module_resources.py:459
+#, python-format
+msgid "Checking events in resource folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:510
-msgid "iTip event without a start"
+#: ../wallace/module_resources.py:475
+#, python-format
+msgid "Fetching message UID %r from folder %r"
 msgstr ""
 
-#. end if c.name == "VEVENT"
-#. end for c in cal.walk()
-#. end if part.get_content_type() == "text/calendar"
-#. end for part in message.walk()
-#. if message.is_multipart()
-#: ../wallace/module_resources.py:540
-msgid "Message is not an iTip message (non-multipart message)"
+#: ../wallace/module_resources.py:498
+#, python-format
+msgid "Event %r conflicts with event %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:561
+#: ../wallace/module_resources.py:525
 #, python-format
-msgid "Checking if email address %r belongs to a resource (collection)"
+msgid "Adding event to %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:572 ../wallace/module_resources.py:646
-#: ../wallace/module_resources.py:696
+#: ../wallace/module_resources.py:573
 #, python-format
-msgid "No resource (collection) records found for %r"
+msgid "Failed to save event to resource calendar at %r: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:590
+#, python-format
+msgid "Delete resource calendar object %r in %r: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:633
+#, python-format
+msgid "Checking if email address %r belongs to a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:580 ../wallace/module_resources.py:654
-#: ../wallace/module_resources.py:704
+#: ../wallace/module_resources.py:641 ../wallace/module_resources.py:709
+#: ../wallace/module_resources.py:743
 #, python-format
 msgid "Resource record(s): %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:586 ../wallace/module_resources.py:661
-#: ../wallace/module_resources.py:711
+#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711
+#: ../wallace/module_resources.py:746
+#, python-format
+msgid "No resource (collection) records found for %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:647 ../wallace/module_resources.py:715
+#: ../wallace/module_resources.py:750
 #, python-format
 msgid "Resource record: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:605
+#: ../wallace/module_resources.py:667
 #, python-format
 msgid "Raw itip_events: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:613
+#: ../wallace/module_resources.py:675
 #, python-format
 msgid "Raw set of attendees: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:621
+#: ../wallace/module_resources.py:683
 #, python-format
 msgid "Raw set of resources: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:635
+#: ../wallace/module_resources.py:702
 #, python-format
 msgid "Checking if attendee %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:668 ../wallace/module_resources.py:714
+#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752
 msgid "Resource reservation made but no resource records found"
 msgstr ""
 
-#: ../wallace/module_resources.py:686
+#: ../wallace/module_resources.py:737
 #, python-format
 msgid "Checking if resource %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:718
+#: ../wallace/module_resources.py:755
 msgid "The following resources are being referred to in the "
 msgstr ""
 
+#: ../wallace/module_resources.py:894
+#, python-format
+msgid ""
+"\n"
+"                *** This is an automated response, please do not reply! ***\n"
+"\n"
+"                Your reservation was delegated to \"%s\" which is available for the requested time.\n"
+"            "
+msgstr ""
+
+#: ../wallace/module_resources.py:905
+#, python-format
+msgid ""
+"\n"
+"        *** This is an automated response, please do not reply! ***\n"
+"        \n"
+"        We hereby inform you that your reservation was %s.\n"
+"    "
+msgstr ""
+
+#: ../wallace/module_resources.py:912
+#, python-format
+msgid ""
+"\n"
+"            If you have questions about this reservation, please contact\n"
+"            %s <%s> %s\n"
+"        "
+msgstr ""
+
+#: ../wallace/module_resources.py:941
+#, python-format
+msgid "Sending booking notification for event %r to %r from %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:954
+msgid "failed"
+msgstr ""
+
+#: ../wallace/module_resources.py:973
+#, python-format
+msgid ""
+"\n"
+"            The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s.\n"
+"\n"
+"            *** This is an automated message, sent to you as the resource owner. ***\n"
+"        "
+msgstr ""
+
+#: ../wallace/module_resources.py:979
+#, python-format
+msgid ""
+"\n"
+"            A reservation request for %(resource)s could not be processed automatically.\n"
+"            Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s.\n"
+"\n"
+"            *** This is an automated message, sent to you as the resource owner. ***\n"
+"        "
+msgstr ""
+
 #. This is a nested module
 #: ../wallace/modules.py:97
 #, python-format
 msgid "Module Group: %s"
 msgstr ""
 
-#: ../wallace/modules.py:108 ../wallace/modules.py:113
-msgid "No such module."
+#: ../wallace/modules.py:108
+#, python-format
+msgid "No such module %r in modules %r (1)."
+msgstr ""
+
+#: ../wallace/modules.py:113
+#, python-format
+msgid "No such module %r in modules %r (2)."
 msgstr ""
 
 #: ../wallace/modules.py:119
@@ -2353,33 +3449,33 @@ msgstr ""
 msgid "Deferring message in %s (by module %s)"
 msgstr ""
 
-#: ../wallace/modules.py:133
+#: ../wallace/modules.py:134
 #, python-format
 msgid "The time when the message was sent: %r"
 msgstr ""
 
-#: ../wallace/modules.py:134
+#: ../wallace/modules.py:135
 #, python-format
 msgid "The time now: %r"
 msgstr ""
 
-#: ../wallace/modules.py:135
+#: ../wallace/modules.py:136
 #, python-format
 msgid "The time delta: %r"
 msgstr ""
 
 #. TODO: Send NDR back to user
-#: ../wallace/modules.py:139
+#: ../wallace/modules.py:140
 #, python-format
 msgid "Message in file %s older then 5 days, deleting"
 msgstr ""
 
-#: ../wallace/modules.py:164
+#: ../wallace/modules.py:165
 #, python-format
 msgid "Rejecting message in %s (by module %s)"
 msgstr ""
 
-#: ../wallace/modules.py:185
+#: ../wallace/modules.py:186
 #, python-format
 msgid ""
 "This is the email system Wallace at %s.\n"
@@ -2394,19 +3490,29 @@ msgid ""
 "recipients.\n"
 msgstr ""
 
-#: ../wallace/modules.py:200
+#: ../wallace/modules.py:201
 #, python-format
 msgid ""
 "X-Wallace-Module: %s\n"
 "X-Wallace-Result: REJECT\n"
 msgstr ""
 
-#: ../wallace/modules.py:253
+#: ../wallace/modules.py:260
 #, python-format
 msgid "Accepting message in %s (by module %s)"
 msgstr ""
 
-#: ../wallace/modules.py:326
+#: ../wallace/modules.py:262
+#, python-format
+msgid "Accepting message in: %r"
+msgstr ""
+
+#: ../wallace/modules.py:269
+#, python-format
+msgid "recipients: %r"
+msgstr ""
+
+#: ../wallace/modules.py:347
 #, python-format
 msgid "Module '%s' already registered"
 msgstr ""
diff --git a/po/pykolab.pot b/po/pykolab.pot
index c3c276a..389ca9b 100644
--- a/po/pykolab.pot
+++ b/po/pykolab.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-02-21 02:15-0500\n"
+"POT-Creation-Date: 2014-07-10 07:21-0400\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
 "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -291,66 +291,66 @@ msgstr ""
 msgid "No objections"
 msgstr ""
 
-#: ../conf.py:37 ../kolab.py:34 ../saslauthd.py:33
+#: ../conf.py:37 ../kolab-cli.py:34 ../saslauthd.py:33
 msgid "Cannot load pykolab/logger.py:"
 msgstr ""
 
 #: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:51
-#: ../wallace/__init__.py:68
+#: ../wallace/__init__.py:85
 msgid "Daemon Options"
 msgstr ""
 
 #: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:58
-#: ../wallace/__init__.py:75
+#: ../wallace/__init__.py:92
 msgid "Fork to the background."
 msgstr ""
 
 #: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:67
-#: ../wallace/__init__.py:101
+#: ../wallace/__init__.py:118
 msgid "Path to the PID file to use."
 msgstr ""
 
 #: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76
-#: ../wallace/__init__.py:118
+#: ../wallace/__init__.py:135
 msgid "Run as user USERNAME"
 msgstr ""
 
 #: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86
-#: ../wallace/__init__.py:92
+#: ../wallace/__init__.py:109
 msgid "Run as group GROUPNAME"
 msgstr ""
 
 #: ../kolabd/__init__.py:122 ../pykolab/logger.py:139 ../pykolab/utils.py:234
-#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:312
+#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:329
 #, python-format
 msgid "Group %s does not exist"
 msgstr ""
 
 #: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301
-#: ../wallace/__init__.py:321
+#: ../wallace/__init__.py:338
 #, python-format
 msgid "Switching real and effective group id to %d"
 msgstr ""
 
 #: ../kolabd/__init__.py:153 ../pykolab/logger.py:159 ../pykolab/utils.py:258
-#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:343
+#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:360
 #, python-format
 msgid "User %s does not exist"
 msgstr ""
 
 #: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333
-#: ../wallace/__init__.py:353
+#: ../wallace/__init__.py:370
 #, python-format
 msgid "Switching real and effective user id to %d"
 msgstr ""
 
 #: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342
-#: ../wallace/__init__.py:362
+#: ../wallace/__init__.py:379
 msgid "Could not change real and effective uid and/or gid"
 msgstr ""
 
 #: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133
-#: ../wallace/__init__.py:382
+#: ../wallace/__init__.py:399
 msgid "Interrupted by user"
 msgstr ""
 
@@ -359,7 +359,7 @@ msgid "Traceback occurred, please report a "
 msgstr ""
 
 #: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141
-#: ../wallace/__init__.py:391
+#: ../wallace/__init__.py:408
 #, python-format
 msgid "Type Error: %s"
 msgstr ""
@@ -368,7 +368,7 @@ msgstr ""
 msgid "Could not connect to LDAP, is it running?"
 msgstr ""
 
-#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2110
+#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137
 #: ../pykolab/cli/cmd_sync.py:36
 msgid "Listing domains..."
 msgstr ""
@@ -442,7 +442,7 @@ msgstr ""
 msgid "Updating timestamp for cache entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/cache.py:154
+#: ../pykolab/auth/ldap/cache.py:155
 #, python-format
 msgid "Updating result_attribute for cache entry %r"
 msgstr ""
@@ -668,95 +668,95 @@ msgstr ""
 msgid "Invalid DN, username and/or password."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1234 ../pykolab/auth/ldap/__init__.py:1247
-#: ../pykolab/auth/ldap/__init__.py:1603 ../pykolab/auth/ldap/__init__.py:1616
+#: ../pykolab/auth/ldap/__init__.py:1236 ../pykolab/auth/ldap/__init__.py:1249
+#: ../pykolab/auth/ldap/__init__.py:1614 ../pykolab/auth/ldap/__init__.py:1627
 #, python-format
 msgid "Found a subject %r with access %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1354
+#: ../pykolab/auth/ldap/__init__.py:1356
 #, python-format
 msgid "Entry %s attribute value: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1362
+#: ../pykolab/auth/ldap/__init__.py:1364
 #, python-format
 msgid "imap.user_mailbox_server(%r) result: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1673 ../pykolab/auth/ldap/__init__.py:1830
+#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853
 #, python-format
 msgid "Result from recipient policy: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1885
+#: ../pykolab/auth/ldap/__init__.py:1908
 #, python-format
 msgid "Kolab user %s does not have a result attribute %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2040
+#: ../pykolab/auth/ldap/__init__.py:2067
 #, python-format
 msgid "Finding domain root dn for domain %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2137
+#: ../pykolab/auth/ldap/__init__.py:2164
 msgid "Authentication database DOWN"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2221 ../pykolab/auth/ldap/__init__.py:2269
+#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296
 #, python-format
 msgid "Entry type: %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2294
+#: ../pykolab/auth/ldap/__init__.py:2321
 #, python-format
 msgid "Done with _synchronize_callback() for entry %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2366
+#: ../pykolab/auth/ldap/__init__.py:2393
 msgid "LDAP Search Result Data Entry:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2382
+#: ../pykolab/auth/ldap/__init__.py:2409
 msgid "Entry Change Notification attributes:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2387
+#: ../pykolab/auth/ldap/__init__.py:2414
 #, python-format
 msgid "Change Type: %r (%r)"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2395
+#: ../pykolab/auth/ldap/__init__.py:2422
 #, python-format
 msgid "Previous DN: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2450
+#: ../pykolab/auth/ldap/__init__.py:2477
 #, python-format
 msgid "Object %s searched no longer exists"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2460
+#: ../pykolab/auth/ldap/__init__.py:2487
 #, python-format
 msgid "%d results..."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2563
+#: ../pykolab/auth/ldap/__init__.py:2590
 #, python-format
 msgid "Searching with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2615
+#: ../pykolab/auth/ldap/__init__.py:2642
 #, python-format
 msgid "Checking for support for %s on %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2634
+#: ../pykolab/auth/ldap/__init__.py:2661
 #, python-format
 msgid "Found support for %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2679
+#: ../pykolab/auth/ldap/__init__.py:2706
 #, python-format
 msgid "An error occured using %s: %r"
 msgstr ""
@@ -916,7 +916,10 @@ msgstr ""
 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:54
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54
 #: ../pykolab/cli/cmd_set_mailbox_metadata.py:66
-#: ../pykolab/cli/cmd_set_quota.py:46
+#: ../pykolab/cli/cmd_set_quota.py:46 ../tests/unit/test-015-translate.py:12
+#: ../tests/unit/test-015-translate.py:16
+#: ../tests/unit/test-015-translate.py:18
+#: ../tests/unit/test-015-translate.py:20
 msgid "Folder name"
 msgstr ""
 
@@ -1063,7 +1066,7 @@ msgstr ""
 
 #: ../pykolab/cli/cmd_remove_user_subscription.py:92
 #, python-format
-msgid "User %s not be unsubscribed from any folders."
+msgid "User %s was not unsubscribed from any folders."
 msgstr ""
 
 #: ../pykolab/cli/cmd_rename_mailbox.py:52
@@ -1432,76 +1435,76 @@ msgstr ""
 msgid "No command supplied"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:411
+#: ../pykolab/conf/__init__.py:416
 msgid "Insufficient options. Need section, key and value -in that order."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:414
+#: ../pykolab/conf/__init__.py:419
 #, python-format
 msgid "No section '%s' exists."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:445
+#: ../pykolab/conf/__init__.py:461
 #, python-format
 msgid "Setting %s to %r (from the default values for CLI options)"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:518
+#: ../pykolab/conf/__init__.py:534
 #, python-format
 msgid "Could not execute configuration function: %s"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:526
+#: ../pykolab/conf/__init__.py:542
 #, python-format
 msgid "Option %s/%s does not exist in config file %s, pulling from defaults"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:534 ../pykolab/conf/__init__.py:537
+#: ../pykolab/conf/__init__.py:550 ../pykolab/conf/__init__.py:553
 msgid "Option does not exist in defaults."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:547
+#: ../pykolab/conf/__init__.py:563
 #, python-format
 msgid "Configuration file %s not readable."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:550
+#: ../pykolab/conf/__init__.py:566
 #, python-format
 msgid "Configuration file %s does not exist."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:555
+#: ../pykolab/conf/__init__.py:571
 msgid ""
 "WARNING: A negative debug level value does not make this program be any more "
 "silent."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:561
+#: ../pykolab/conf/__init__.py:577
 msgid "This program has 9 levels of verbosity. Using the maximum of 9."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:569 ../pykolab/conf/__init__.py:575
+#: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591
 msgid "Cannot start SASL authentication daemon"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:586
+#: ../pykolab/conf/__init__.py:602
 msgid "No imaplib library found."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:596
+#: ../pykolab/conf/__init__.py:612
 msgid "No LMTP class found in the smtplib library."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:606
+#: ../pykolab/conf/__init__.py:622
 msgid "No SMTP class found in the smtplib library."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:620
+#: ../pykolab/conf/__init__.py:636
 #, python-format
 msgid "Found you specified a specific set of items to test: %s"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:628
+#: ../pykolab/conf/__init__.py:644
 #, python-format
 msgid "Selectively selecting: %s"
 msgstr ""
@@ -1686,169 +1689,174 @@ msgstr ""
 msgid "%r has no attribute %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:390 ../pykolab/imap/__init__.py:425
+#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428
 #, python-format
 msgid "Creating new shared folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:450 ../pykolab/imap/__init__.py:672
+#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675
 #, python-format
 msgid "Downcasing mailbox name %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:454
+#: ../pykolab/imap/__init__.py:457
 #, python-format
 msgid "Creating new mailbox for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:467
+#: ../pykolab/imap/__init__.py:470
 msgid "Waiting for the Cyrus IMAP Murder to settle..."
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:513
+#: ../pykolab/imap/__init__.py:516
 #, python-format
 msgid "Creating additional folders for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:532
+#: ../pykolab/imap/__init__.py:535
 #, python-format
 msgid "Waiting for the Cyrus murder to settle... %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:544
+#: ../pykolab/imap/__init__.py:547
 #, python-format
 msgid "Correcting additional folder name from %r to %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:550
+#: ../pykolab/imap/__init__.py:553
 #, python-format
 msgid "Mailbox already exists: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:590
+#: ../pykolab/imap/__init__.py:593
 msgid "Subscribing user to the additional folders"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:604
+#: ../pykolab/imap/__init__.py:607
 msgid "Using the following tests for folder subscriptions:"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:606
+#: ../pykolab/imap/__init__.py:609
 #, python-format
 msgid "    %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:609
+#: ../pykolab/imap/__init__.py:612
 #, python-format
 msgid "Folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:621
+#: ../pykolab/imap/__init__.py:624
 #, python-format
 msgid "Subscribing %s to folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:625
+#: ../pykolab/imap/__init__.py:628
 #, python-format
 msgid "Subscribing %s to folder %s failed: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:655
+#: ../pykolab/imap/__init__.py:658
 #, python-format
 msgid "Could not rename %s to reside on partition %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:688 ../pykolab/imap/__init__.py:764
+#: ../pykolab/imap/__init__.py:691
+#, python-format
+msgid "INBOX folder to rename (%s) does not exist"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770
 #, python-format
 msgid "Renaming INBOX from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:692
+#: ../pykolab/imap/__init__.py:698
 #, python-format
 msgid "Could not rename INBOX folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:768
+#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774
 #, python-format
 msgid "Moving INBOX folder %s won't succeed as target folder %s already exists"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:698
+#: ../pykolab/imap/__init__.py:704
 #, python-format
 msgid "Server for mailbox %r is %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:706
+#: ../pykolab/imap/__init__.py:712
 #, python-format
 msgid "Looking for folder '%s', we found folders: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:729
+#: ../pykolab/imap/__init__.py:735
 #, python-format
 msgid "Setting ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:740
+#: ../pykolab/imap/__init__.py:746
 #, python-format
 msgid "Removing ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:761
+#: ../pykolab/imap/__init__.py:767
 #, python-format
 msgid "Found old INBOX folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:770
+#: ../pykolab/imap/__init__.py:776
 #, python-format
 msgid "Did not find old folder user/%s to rename"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:772
+#: ../pykolab/imap/__init__.py:778
 msgid "Value for user is not a dictionary"
 msgstr ""
 
 #. TODO: Go in fact correct the quota.
-#: ../pykolab/imap/__init__.py:840
+#: ../pykolab/imap/__init__.py:846
 #, python-format
 msgid "Cannot get current IMAP quota for folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:853
+#: ../pykolab/imap/__init__.py:859
 #, python-format
 msgid "Quota for %s currently is %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:859
+#: ../pykolab/imap/__init__.py:865
 #, python-format
 msgid "Adjusting authentication database quota for folder %s to %d"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:864
+#: ../pykolab/imap/__init__.py:870
 #, python-format
 msgid "Correcting quota for %s to %s (currently %s)"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:941
+#: ../pykolab/imap/__init__.py:947
 #, python-format
 msgid "Checking folder: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:946
+#: ../pykolab/imap/__init__.py:952
 #, python-format
 msgid "Folder has no corresponding user (1): %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:949
+#: ../pykolab/imap/__init__.py:955
 #, python-format
 msgid "Folder has no corresponding user (2): %s"
 msgstr ""
 
 #. We got user identifier only
-#: ../pykolab/imap/__init__.py:964
+#: ../pykolab/imap/__init__.py:970
 msgid "Please don't give us just a user identifier"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:967
+#: ../pykolab/imap/__init__.py:973
 #, python-format
 msgid "Deleting folder %s"
 msgstr ""
@@ -1857,6 +1865,44 @@ msgstr ""
 msgid "Returning thread local configuration"
 msgstr ""
 
+#: ../pykolab/itip/__init__.py:43
+#, python-format
+msgid "Method %r not really interesting for us."
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:49
+#, python-format
+msgid "Raw iTip payload: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:59
+msgid "Could not read iTip from message."
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:67
+#, python-format
+msgid "Duplicate iTip object: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:90
+msgid "iTip event without a start"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:132
+msgid "Message is not an iTip message (non-multipart message)"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:225
+#, python-format
+msgid "Failed to compose iTip reply message: %r"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936
+#: ../wallace/module_resources.py:964
+#, python-format
+msgid "SMTP sendmail error: %r"
+msgstr ""
+
 #: ../pykolab/logger.py:173 ../pykolab/logger.py:179
 #, python-format
 msgid "Could not change permissions on %s: %r"
@@ -1991,6 +2037,16 @@ msgid ""
 "Attribute substitution for 'alternative_mail' failed in Recipient Policy"
 msgstr ""
 
+#: ../pykolab/plugins/roundcubedb/__init__.py:48
+#, python-format
+msgid "user_delete: %r"
+msgstr ""
+
+#: ../pykolab/plugins/roundcubedb/__init__.py:55
+#: ../pykolab/setup/setup_roundcube.py:160
+msgid "Roundcube installation path not found."
+msgstr ""
+
 #: ../pykolab/plugins/sievemgmt/__init__.py:51
 msgid "Wrong number of arguments for sieve management plugin"
 msgstr ""
@@ -2007,11 +2063,11 @@ msgstr ""
 msgid "No such component."
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:45
+#: ../pykolab/setup/setup_freebusy.py:46
 msgid "Setup Free/Busy."
 msgstr ""
 
-#: ../pykolab/setup/setup_freebusy.py:49
+#: ../pykolab/setup/setup_freebusy.py:50
 msgid "Free/Busy is not installed on this system"
 msgstr ""
 
@@ -2033,7 +2089,7 @@ msgstr ""
 
 #: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:81
 #: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:455
-#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:234
+#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:237
 #: ../pykolab/setup/setup_syncroton.py:102
 msgid "Could not configure to start on boot, the "
 msgstr ""
@@ -2371,10 +2427,6 @@ msgstr ""
 msgid "Could not start the MySQL database service."
 msgstr ""
 
-#. Regular old-fashioned Enterprise Linux
-#. Debian
-#. (open)SUSE
-#. "Unbreakable" Linux from Oracle
 #: ../pykolab/setup/setup_mysql.py:71
 msgid "What MySQL server are we setting up?"
 msgstr ""
@@ -2390,7 +2442,7 @@ msgid ""
 msgstr ""
 
 #: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99
-#: ../pykolab/setup/setup_roundcube.py:180
+#: ../pykolab/setup/setup_roundcube.py:183
 #: ../pykolab/setup/setup_syncroton.py:63
 msgid "MySQL root password"
 msgstr ""
@@ -2486,21 +2538,17 @@ msgstr ""
 msgid "MySQL roundcube password"
 msgstr ""
 
-#: ../pykolab/setup/setup_roundcube.py:117
+#: ../pykolab/setup/setup_roundcube.py:120
 #, python-format
 msgid "Using template file %r"
 msgstr ""
 
-#: ../pykolab/setup/setup_roundcube.py:124
+#: ../pykolab/setup/setup_roundcube.py:127
 #, python-format
 msgid "Successfully compiled template %r, writing out to %r"
 msgstr ""
 
-#: ../pykolab/setup/setup_roundcube.py:157
-msgid "Roundcube installation path not found."
-msgstr ""
-
-#: ../pykolab/setup/setup_roundcube.py:225
+#: ../pykolab/setup/setup_roundcube.py:228
 #: ../pykolab/setup/setup_syncroton.py:93
 msgid "Could not start the webserver server service."
 msgstr ""
@@ -2570,29 +2618,29 @@ msgstr ""
 msgid "Could not change the permissions on %s"
 msgstr ""
 
-#: ../pykolab/utils.py:476
+#: ../pykolab/utils.py:479
 #, python-format
 msgid "Transliterating string %r with locale %r"
 msgstr ""
 
-#: ../pykolab/utils.py:484
+#: ../pykolab/utils.py:487
 msgid "Attempting to set locale"
 msgstr ""
 
-#: ../pykolab/utils.py:486
+#: ../pykolab/utils.py:489
 msgid "Success setting locale"
 msgstr ""
 
-#: ../pykolab/utils.py:488
+#: ../pykolab/utils.py:491
 msgid "Failure to set locale"
 msgstr ""
 
-#: ../pykolab/utils.py:496
+#: ../pykolab/utils.py:499
 #, python-format
 msgid "Executing '%s | %s'"
 msgstr ""
 
-#: ../pykolab/utils.py:507
+#: ../pykolab/utils.py:510
 #, python-format
 msgid "Could not translate %s using locale %s"
 msgstr ""
@@ -2608,90 +2656,124 @@ msgid "Got response: %r"
 msgstr ""
 
 #. Some data is not JSON
-#: ../pykolab/wap_client/__init__.py:333
+#: ../pykolab/wap_client/__init__.py:334
 msgid "Response data is not JSON"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:86 ../pykolab/xml/attendee.py:108
+#. support integer values, too
+#: ../pykolab/xml/attendee.py:9 ../pykolab/xml/attendee.py:17
+msgid "Needs Action"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:10 ../pykolab/xml/attendee.py:18
+msgid "Accepted"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:11 ../pykolab/xml/attendee.py:19
+msgid "Declined"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:12 ../pykolab/xml/attendee.py:20
+msgid "Tentatively Accepted"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:13 ../pykolab/xml/attendee.py:21
+msgid "Delegated"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:14
+msgid "Completed"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:15
+msgid "In Process"
+msgstr ""
+
+#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130
 msgid "Not a valid attendee"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:93
+#: ../pykolab/xml/attendee.py:115
 msgid "No valid delegator references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:113
+#: ../pykolab/xml/attendee.py:135
 msgid "No valid delegatee references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:149
+#: ../pykolab/xml/attendee.py:180
 #, python-format
 msgid "Invalid cutype %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:160
+#: ../pykolab/xml/attendee.py:192
 #, python-format
 msgid "Invalid participant status %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:168
+#: ../pykolab/xml/attendee.py:200
 #, python-format
 msgid "Invalid role %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:70 ../pykolab/xml/event.py:570
-#: ../pykolab/xml/event.py:606
+#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708
+#: ../pykolab/xml/event.py:751
 msgid "Event start needs datetime.date or datetime.datetime instance"
 msgstr ""
 
-#: ../pykolab/xml/event.py:198
+#: ../pykolab/xml/event.py:241
 #, python-format
 msgid "No attendee with email or name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:206
+#: ../pykolab/xml/event.py:249
 #, python-format
 msgid "Invalid argument value attendee %r, must be basestring or Attendee"
 msgstr ""
 
-#: ../pykolab/xml/event.py:212
+#: ../pykolab/xml/event.py:255
 #, python-format
 msgid "No attendee with email %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:218
+#: ../pykolab/xml/event.py:261
 #, python-format
 msgid "No attendee with name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:323
+#: ../pykolab/xml/event.py:426
 msgid "Invalid participant status"
 msgstr ""
 
-#: ../pykolab/xml/event.py:445
-msgid "Event end needs datetime.date or datetime.datetime instance"
+#: ../pykolab/xml/event.py:542
+#, python-format
+msgid "Invalid status %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:549
+#: ../pykolab/xml/event.py:550
 #, python-format
-msgid "Invalid status %r"
+msgid "Invalid classification %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:616
+#: ../pykolab/xml/event.py:577
+msgid "Event end needs datetime.date or datetime.datetime instance"
+msgstr ""
+
+#: ../pykolab/xml/event.py:761
 #, python-format
 msgid "Invalid status set: %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:735
+#: ../pykolab/xml/event.py:923
 msgid "No sender specified"
 msgstr ""
 
-#: ../pykolab/xml/event.py:744
+#: ../pykolab/xml/event.py:932
 #, python-format
-msgid "Reservation Request for %s was %s"
+msgid "Invitation for %s was %s"
 msgstr ""
 
-#: ../pykolab/xml/event.py:749
+#: ../pykolab/xml/event.py:937
 msgid "This is an automated response to one of your event requests."
 msgstr ""
 
@@ -2701,7 +2783,7 @@ msgid "Could not create %r: %r"
 msgstr ""
 
 #: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145
-#: ../wallace/__init__.py:386 ../wallace/__init__.py:395
+#: ../wallace/__init__.py:403 ../wallace/__init__.py:412
 msgid "Traceback occurred, please report a bug at http://bugzilla.kolabsys.com"
 msgstr ""
 
@@ -2713,51 +2795,101 @@ msgstr ""
 msgid "Maximum tries exceeded, exiting"
 msgstr ""
 
-#: ../wallace/__init__.py:61
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190
+#: ../wallace/module_resources.py:879
+#, python-format
+msgid "Reservation Request for %(summary)s was %(status)s"
+msgstr ""
+
+#. check notification message sent to resource owner (jane)
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:605
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:621
+#: ../wallace/module_resources.py:954
+#, python-format
+msgid "Booking for %s has been %s"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:146
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:720
+#: ../wallace/module_invitationpolicy.py:374
+#, python-format
+msgid "\"%(summary)s\" has been %(status)s"
+msgstr ""
+
+#. check for notification message
+#. this notification should be suppressed until mark has replied, too
+#. this triggers an additional notification
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:616
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:622
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:635
+#: ../wallace/module_invitationpolicy.py:925
+#, python-format
+msgid "\"%s\" has been updated"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:627
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639
+msgid "PENDING"
+msgstr ""
+
+#: ../wallace/__init__.py:57
+#, python-format
+msgid "Wallace modules: %r"
+msgstr ""
+
+#: ../wallace/__init__.py:69
+#, python-format
+msgid "Module %s.execute() failed on message %r with error: %s"
+msgstr ""
+
+#: ../wallace/__init__.py:78
 #, python-format
 msgid "Worker process %s initializing"
 msgstr ""
 
-#: ../wallace/__init__.py:83
+#: ../wallace/__init__.py:100
 msgid "Bind address for Wallace."
 msgstr ""
 
-#: ../wallace/__init__.py:109
+#: ../wallace/__init__.py:126
 msgid "Port that Wallace is supposed to use."
 msgstr ""
 
-#: ../wallace/__init__.py:160
+#: ../wallace/__init__.py:177
 #, python-format
 msgid "Could not bind to socket on port %d on bind "
 msgstr ""
 
-#: ../wallace/__init__.py:172
+#: ../wallace/__init__.py:189
 msgid "Could not shut down socket"
 msgstr ""
 
-#: ../wallace/__init__.py:236
+#: ../wallace/__init__.py:253
 msgid "Accepted connection"
 msgstr ""
 
-#: ../wallace/__init__.py:411
+#: ../wallace/__init__.py:428
 #, python-format
 msgid "Could not write pid file %s"
 msgstr ""
 
-#: ../wallace/module_footer.py:60 ../wallace/module_optout.py:61
-#: ../wallace/module_resources.py:109
+#: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60
+#: ../wallace/module_invitationpolicy.py:168 ../wallace/module_optout.py:61
+#: ../wallace/module_resources.py:120
 #, python-format
 msgid "Issuing callback after processing to stage %s"
 msgstr ""
 
-#: ../wallace/module_footer.py:61 ../wallace/module_optout.py:62
-#: ../wallace/module_resources.py:115
+#: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61
+#: ../wallace/module_invitationpolicy.py:170 ../wallace/module_optout.py:62
+#: ../wallace/module_resources.py:126
 #, python-format
 msgid "Testing cb_action_%s()"
 msgstr ""
 
-#: ../wallace/module_footer.py:63 ../wallace/module_optout.py:64
-#: ../wallace/module_resources.py:118
+#: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63
+#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:64
+#: ../wallace/module_resources.py:129
 #, python-format
 msgid "Attempting to execute cb_action_%s()"
 msgstr ""
@@ -2767,6 +2899,299 @@ msgstr ""
 msgid "Executing module footer for %r, %r"
 msgstr ""
 
+#: ../wallace/module_gpgencrypt.py:66
+#, python-format
+msgid "Executing module gpgencrypt for %r, %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:98
+msgid "Message is already encrypted (app/pgp-enc content-type)"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:102
+msgid "Message already encrypted by main content-type header"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:131
+msgid ""
+"Configured to encrypt to a key not configured, and strict policy enabled. "
+"Bailing out."
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:134
+msgid ""
+"Configured to encrypt to a key not configured, but continuing anyway (see "
+"'gpgencrypt_strict')."
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:171
+#, python-format
+msgid "Recipients: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:183
+#, python-format
+msgid "Current keys: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:188
+#, python-format
+msgid "Retrieving key for recipient: %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:192 ../wallace/module_gpgencrypt.py:208
+#, python-format
+msgid "Found matching address %r"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:200
+#, python-format
+msgid "Found matching address %r in remote keys"
+msgstr ""
+
+#: ../wallace/module_gpgencrypt.py:232
+#, python-format
+msgid "An error occurred: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:154
+#, python-format
+msgid "Invitation policy called for %r, %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:211
+#: ../wallace/module_resources.py:169
+#, python-format
+msgid "Failed to parse iTip events from message: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:215
+msgid ""
+"Message is not an iTip message or does not contain any (valid) iTip events."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:219
+#, python-format
+msgid ""
+"iTip events attached to this message contain the following information: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:232
+#, python-format
+msgid "No itips, no users, pass along %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:235
+#, python-format
+msgid "iTips, but no users, pass along %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:255
+#, python-format
+msgid "No user attendee matching envelope recipient %s, skip message"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:259
+#, python-format
+msgid "Receiving user: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:284
+#, python-format
+msgid "Apply invitation policy %r for domain %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:295
+#, python-format
+msgid "Ignoring '%s' iTip method"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:299
+#, python-format
+msgid "iTip message %r consumed by the invitationpolicy module"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:315
+msgid "Pass invitation for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:320
+#, python-format
+msgid "Receiving Attendee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:339
+#, python-format
+msgid "Existing event: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:350
+#, python-format
+msgid "Precondition for event %r fulfilled: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:386
+#, python-format
+msgid "No RSVP for recipient %r requested"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:412
+msgid "Pass reply for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:419
+#, python-format
+msgid "Sender Attendee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:431
+#, python-format
+msgid ""
+"The iTip reply sequence (%r) doesn't match the referred event version (%r). "
+"Forwarding to Inbox."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:437
+#, python-format
+msgid "Auto-updating event %r on iTip REPLY"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:459
+#: ../wallace/module_invitationpolicy.py:488
+msgid ""
+"The event referred by this reply was not found in the user's calendars. "
+"Forwarding to Inbox."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:472
+msgid "Pass cancellation for manual processing"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:517
+#, python-format
+msgid "Checking if email address %r belongs to a local user"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:522
+#, python-format
+msgid "User DN: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:524
+#, python-format
+msgid "No user record(s) found for %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:577
+#, python-format
+msgid "User record doesn't have the mailbox attribute %r set"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:590
+#, python-format
+msgid "IMAP proxy authentication failed: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:612
+#, python-format
+msgid "List calendar folders for user %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:628
+#, python-format
+msgid "IMAP metadata for %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:658
+#, python-format
+msgid "Searching folder %r for event %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:670
+#: ../wallace/module_invitationpolicy.py:709
+#: ../wallace/module_resources.py:486
+#, python-format
+msgid "Failed to parse event from message %s/%s: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:696
+#, python-format
+msgid "Listing events from folder %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:715
+#, python-format
+msgid "Existing event %r conflicts with invitation %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:722
+#: ../wallace/module_resources.py:344
+#, python-format
+msgid "start: %r, end: %r, total: %r, messages: %d"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:748
+#, python-format
+msgid "%r is locked, waiting..."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:811
+#, python-format
+msgid "Failed to save event: no calendar folder found for user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:814
+#, python-format
+msgid "Save event %r to user calendar %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:827
+#, python-format
+msgid "Failed to save event to user calendar at %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:843
+#, python-format
+msgid "Delete event %r in %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:863
+#, python-format
+msgid "Compose participation status summary for event %r to user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:901
+#, python-format
+msgid ""
+"Waiting for more automated replies (got %d of %d); skipping notification"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:998
+#, python-format
+msgid "Updated %s's copy of %r: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1001
+#, python-format
+msgid "Attendee %s's copy of %r not found"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1004
+#, python-format
+msgid "Attendee %r not found in LDAP"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1008
+#, python-format
+msgid ""
+"\n"
+"        %(name)s has %(status)s your invitation for %(summary)s.\n"
+"\n"
+"        *** This is an automated response sent by the Kolab Invitation "
+"system ***\n"
+"    "
+msgstr ""
+
 #. modules.next_module('optout')
 #: ../wallace/module_optout.py:70
 #, python-format
@@ -2788,220 +3213,262 @@ msgstr ""
 msgid "Could not send request to optout_url %s"
 msgstr ""
 
-#: ../wallace/module_resources.py:96
+#: ../wallace/module_resources.py:110
 #, python-format
 msgid "Resource Management called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:159
+#: ../wallace/module_resources.py:174
 msgid "Message is not an iTip message or does not contain any "
 msgstr ""
 
-#: ../wallace/module_resources.py:167
+#: ../wallace/module_resources.py:182
 msgid "iTip events attached to this message contain the "
 msgstr ""
 
-#: ../wallace/module_resources.py:188
+#: ../wallace/module_resources.py:205
 msgid "Not an iTip message, but sent to resource nonetheless. Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:196
-msgid "No itips, no resources, pass along"
+#: ../wallace/module_resources.py:213
+#, python-format
+msgid "No itips, no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:200
-msgid "iTips, but no resources, pass along"
+#: ../wallace/module_resources.py:216
+#, python-format
+msgid "iTips, but no resources, pass along %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:225
+#, python-format
+msgid "No resource attendees matching envelope recipient %s, Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:233
+#: ../wallace/module_resources.py:234
 #, python-format
 msgid "Resources: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:242
+#: ../wallace/module_resources.py:244
 #, python-format
 msgid "Receiving Resource: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:250
+#: ../wallace/module_resources.py:252
 #, python-format
 msgid "Recipient %r is non-participant, ignoring message"
 msgstr ""
 
-#: ../wallace/module_resources.py:281
+#: ../wallace/module_resources.py:279
 #, python-format
-msgid "Failed to read resource calendar for %r: %r"
+msgid "Accept invitation for individual resource %r / %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:286
+#: ../wallace/module_resources.py:308
 #, python-format
-msgid "start: %r, end: %r, total: %r, messages: %d"
+msgid "Delegate invitation for resource collection %r to %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:340
+#, python-format
+msgid "Failed to read resource calendar for %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:292
+#: ../wallace/module_resources.py:350
 #, python-format
 msgid "Polling for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:295
+#: ../wallace/module_resources.py:353
 #, python-format
 msgid "Resource %r has been popped from the list"
 msgstr ""
 
-#: ../wallace/module_resources.py:299
+#: ../wallace/module_resources.py:357
 msgid "Resource is a collection"
 msgstr ""
 
-#: ../wallace/module_resources.py:310
+#: ../wallace/module_resources.py:368
 #, python-format
 msgid "Removed conflicting resources from %r: (%r) => %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:322
+#: ../wallace/module_resources.py:380
 #, python-format
 msgid "Conflicting events: %r for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:351
+#: ../wallace/module_resources.py:397
 #, python-format
-msgid "Accept invitation for individual resource %r / %r"
+msgid "Delegate to another resource collection member: %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:365
-#, python-format
-msgid "Delegate invitation for resource collection %r to %r"
-msgstr ""
-
-#: ../wallace/module_resources.py:408
+#: ../wallace/module_resources.py:459
 #, python-format
 msgid "Checking events in resource folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:424
+#: ../wallace/module_resources.py:475
 #, python-format
 msgid "Fetching message UID %r from folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:472
+#: ../wallace/module_resources.py:498
 #, python-format
 msgid "Event %r conflicts with event %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:499
+#: ../wallace/module_resources.py:525
 #, python-format
 msgid "Adding event to %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:537
+#: ../wallace/module_resources.py:573
 #, python-format
 msgid "Failed to save event to resource calendar at %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:553
+#: ../wallace/module_resources.py:590
 #, python-format
 msgid "Delete resource calendar object %r in %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:587
-#, python-format
-msgid "Method %r not really interesting for us."
-msgstr ""
-
-#: ../wallace/module_resources.py:595
-#, python-format
-msgid "Raw iTip payload: %s"
-msgstr ""
-
-#: ../wallace/module_resources.py:605
-msgid "Could not read iTip from message."
-msgstr ""
-
-#: ../wallace/module_resources.py:613
-#, python-format
-msgid "Duplicate iTip event: %s"
-msgstr ""
-
-#: ../wallace/module_resources.py:638
-msgid "iTip event without a start"
-msgstr ""
-
-#: ../wallace/module_resources.py:676
-msgid "Message is not an iTip message (non-multipart message)"
-msgstr ""
-
-#: ../wallace/module_resources.py:705
+#: ../wallace/module_resources.py:633
 #, python-format
 msgid "Checking if email address %r belongs to a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:713 ../wallace/module_resources.py:781
-#: ../wallace/module_resources.py:815
+#: ../wallace/module_resources.py:641 ../wallace/module_resources.py:709
+#: ../wallace/module_resources.py:743
 #, python-format
 msgid "Resource record(s): %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:715 ../wallace/module_resources.py:783
-#: ../wallace/module_resources.py:818
+#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711
+#: ../wallace/module_resources.py:746
 #, python-format
 msgid "No resource (collection) records found for %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:719 ../wallace/module_resources.py:787
-#: ../wallace/module_resources.py:822
+#: ../wallace/module_resources.py:647 ../wallace/module_resources.py:715
+#: ../wallace/module_resources.py:750
 #, python-format
 msgid "Resource record: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:739
+#: ../wallace/module_resources.py:667
 #, python-format
 msgid "Raw itip_events: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:747
+#: ../wallace/module_resources.py:675
 #, python-format
 msgid "Raw set of attendees: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:755
+#: ../wallace/module_resources.py:683
 #, python-format
 msgid "Raw set of resources: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:774
+#: ../wallace/module_resources.py:702
 #, python-format
 msgid "Checking if attendee %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:790 ../wallace/module_resources.py:824
+#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752
 msgid "Resource reservation made but no resource records found"
 msgstr ""
 
-#: ../wallace/module_resources.py:809
+#: ../wallace/module_resources.py:737
 #, python-format
 msgid "Checking if resource %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:827
+#: ../wallace/module_resources.py:755
 msgid "The following resources are being referred to in the "
 msgstr ""
 
-#: ../wallace/module_resources.py:867
+#: ../wallace/module_resources.py:894
 #, python-format
 msgid ""
 "\n"
-"                Your reservation was delegated to \"%s\"\n"
-"                which is available for the requested time.\n"
+"                *** This is an automated response, please do not reply! ***\n"
+"\n"
+"                Your reservation was delegated to \"%s\" which is available "
+"for the requested time.\n"
 "            "
 msgstr ""
 
+#: ../wallace/module_resources.py:905
+#, python-format
+msgid ""
+"\n"
+"        *** This is an automated response, please do not reply! ***\n"
+"        \n"
+"        We hereby inform you that your reservation was %s.\n"
+"    "
+msgstr ""
+
+#: ../wallace/module_resources.py:912
+#, python-format
+msgid ""
+"\n"
+"            If you have questions about this reservation, please contact\n"
+"            %s <%s> %s\n"
+"        "
+msgstr ""
+
+#: ../wallace/module_resources.py:941
+#, python-format
+msgid "Sending booking notification for event %r to %r from %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:954
+msgid "failed"
+msgstr ""
+
+#: ../wallace/module_resources.py:973
+#, python-format
+msgid ""
+"\n"
+"            The resource booking for %(resource)s by %(orgname)s <%(orgemail)"
+"s> has been %(status)s for %(date)s.\n"
+"\n"
+"            *** This is an automated message, sent to you as the resource "
+"owner. ***\n"
+"        "
+msgstr ""
+
+#: ../wallace/module_resources.py:979
+#, python-format
+msgid ""
+"\n"
+"            A reservation request for %(resource)s could not be processed "
+"automatically.\n"
+"            Please contact %(orgname)s <%(orgemail)s> who requested this "
+"resource for %(date)s. Subject: %(summary)s.\n"
+"\n"
+"            *** This is an automated message, sent to you as the resource "
+"owner. ***\n"
+"        "
+msgstr ""
+
 #. This is a nested module
 #: ../wallace/modules.py:97
 #, python-format
 msgid "Module Group: %s"
 msgstr ""
 
-#: ../wallace/modules.py:108 ../wallace/modules.py:113
-msgid "No such module."
+#: ../wallace/modules.py:108
+#, python-format
+msgid "No such module %r in modules %r (1)."
+msgstr ""
+
+#: ../wallace/modules.py:113
+#, python-format
+msgid "No such module %r in modules %r (2)."
 msgstr ""
 
 #: ../wallace/modules.py:119


commit 5d97d7f2da3d97930941c081c8bbba7de687863f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 10 06:26:46 2014 -0400

    Implement gettext language switch; remove en.po as this is not used

diff --git a/po/en.po b/po/en.po
deleted file mode 100644
index da6a905..0000000
--- a/po/en.po
+++ /dev/null
@@ -1,15 +0,0 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# 
-msgid ""
-msgstr ""
-"Project-Id-Version: Kolab Groupware Solution\n"
-"Report-Msgid-Bugs-To: https://isues.kolab.org/\n"
-"POT-Creation-Date: 2014-07-17 10:22+0100\n"
-"PO-Revision-Date: 2014-07-14 11:13+0000\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: en\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
diff --git a/pykolab/translate.py b/pykolab/translate.py
index 85f4516..080cbc2 100644
--- a/pykolab/translate.py
+++ b/pykolab/translate.py
@@ -26,9 +26,10 @@ import gettext
 import os
 
 N_ = lambda x: x
-_ = lambda x: gettext.ldgettext(domain, x)
+_ = lambda x: current.lgettext(x)
 
-#gettext.bindtextdomain(domain, '/usr/local/share/locale')
+localedir = '/usr/local/share/locale'
+current = gettext.translation(domain, localedir, fallback=True)
 
 def getDefaultLangs():
     languages = []
@@ -49,15 +50,14 @@ def getDefaultLangs():
     return nelangs
 
 def setUserLanguage(lang):
+    global current
+
     langs = []
     for l in gettext._expand_lang(lang):
         if l not in langs:
             langs.append(l)
 
     try:
-        translation = gettext.translation(domain, languages=langs)
-        translation.install()
+        current = gettext.translation(domain, localedir, languages=langs, fallback=True)
     except:
-        return False
-
-    return True
+        pass
diff --git a/tests/unit/test-015-translate.py b/tests/unit/test-015-translate.py
index 8ca9463..6819b80 100644
--- a/tests/unit/test-015-translate.py
+++ b/tests/unit/test-015-translate.py
@@ -4,9 +4,6 @@ from pykolab import translate
 
 class TestTranslate(unittest.TestCase):
 
-    def setUp(self):
-        translate.setUserLanguage('en')
-
     def test_001_default_langs(self):
         self.assertTrue(len(translate.getDefaultLangs()) > 0)
 
@@ -16,10 +13,11 @@ class TestTranslate(unittest.TestCase):
 
     def test_003_set_lang(self):
         from pykolab.translate import _
-        self.assertFalse(translate.setUserLanguage('foo_bar'))
         self.assertEqual(_("Folder name"), "Folder name")
-        self.assertTrue(translate.setUserLanguage('de_DE'))
-        self.assertEqual(_("Folder name"), "Ordnername")
+        translate.setUserLanguage('de_DE')
+        self.assertEqual(_("Folder name"), "Ordnername", "German Translation found")
+        translate.setUserLanguage('foo_bar')
+        self.assertEqual(_("Folder name"), "Folder name", "Unkonwn language falls back to NullTranslations")
 
 if __name__ == '__main__':
     unittest.main()


commit 6b3df45c2cc13f5fcc3320404881cbff0a8f012e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 10 05:17:01 2014 -0400

    Use deferred translations

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 220ab8c..a01d955 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -1,27 +1,28 @@
 import kolabformat
 
 from pykolab.translate import _
+from pykolab.translate import N_
 
 from contact_reference import ContactReference
 
 participant_status_labels = {
-        "NEEDS-ACTION": _("Needs Action"),
-        "ACCEPTED": _("Accepted"),
-        "DECLINED": _("Declined"),
-        "TENTATIVE": _("Tentatively Accepted"),
-        "DELEGATED": _("Delegated"),
-        "COMPLETED": _("Completed"),
-        "IN-PROCESS": _("In Process"),
+        "NEEDS-ACTION": N_("Needs Action"),
+        "ACCEPTED": N_("Accepted"),
+        "DECLINED": N_("Declined"),
+        "TENTATIVE": N_("Tentatively Accepted"),
+        "DELEGATED": N_("Delegated"),
+        "COMPLETED": N_("Completed"),
+        "IN-PROCESS": N_("In Process"),
         # support integer values, too
-        kolabformat.PartNeedsAction: _("Needs Action"),
-        kolabformat.PartAccepted: _("Accepted"),
-        kolabformat.PartDeclined: _("Declined"),
-        kolabformat.PartTentative: _("Tentatively Accepted"),
-        kolabformat.PartDelegated: _("Delegated"),
+        kolabformat.PartNeedsAction: N_("Needs Action"),
+        kolabformat.PartAccepted: N_("Accepted"),
+        kolabformat.PartDeclined: N_("Declined"),
+        kolabformat.PartTentative: N_("Tentatively Accepted"),
+        kolabformat.PartDelegated: N_("Delegated"),
     }
 
 def participant_status_label(status):
-    return participant_status_labels[status] if participant_status_labels.has_key(status) else status
+    return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else status
 
 
 class Attendee(kolabformat.Attendee):


commit c395789e553531a4a565bcc61a422255ef5385b4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 10 05:09:11 2014 -0400

    Send consolidated update notifications to an event organizer. This means suppressing notifications triggered by wallace replies as long as more automated replies can be expected; Use localized participant status texts in iTip messages

diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 8dc3ff7..dd419f0 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -12,6 +12,7 @@ from wallace import module_resources
 
 from pykolab.translate import _
 from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
 from email import message_from_string
 from twisted.trial import unittest
 
@@ -177,6 +178,16 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
         }
 
+        self.bob = {
+            'displayname': 'Bob Auto',
+            'mail': 'bob.auto at example.org',
+            'dn': 'uid=auto,ou=People,dc=example,dc=org',
+            'preferredlanguage': 'en_US',
+            'mailbox': 'user/bob.auto at example.org',
+            'kolabtargetfolder': 'user/bob.auto/Calendar at example.org',
+            'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE']
+        }
+
         self.external = {
             'displayname': 'Bob External',
             'mail': 'bob.external at gmail.com'
@@ -186,6 +197,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage'])
         user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage'])
         user_add("Jack", "Tentative", kolabinvitationpolicy=self.jack['kolabinvitationpolicy'], preferredlanguage=self.jack['preferredlanguage'])
+        user_add("Bob", "Auto", kolabinvitationpolicy=self.bob['kolabinvitationpolicy'], preferredlanguage=self.bob['preferredlanguage'])
 
         time.sleep(1)
         from tests.functional.synchronize import synchronize_once
@@ -432,7 +444,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,13, 10,0,0)
         uid = self.send_itip_invitation(self.jane['mail'], start)
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -453,7 +465,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
     def test_002_invite_conflict_reject(self):
         uid = self.send_itip_invitation(self.jane['mail'], datetime.datetime(2014,8,13, 11,0,0), summary="test2")
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':_('DECLINED') }, self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -466,7 +478,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,24, 8,0,0))
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
         self.assertIsInstance(response, email.message.Message)
 
 
@@ -474,12 +486,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.purge_mailbox(self.john['mailbox'])
 
         self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 8,0,0))
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         # send conflicting request to jack
         uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 10,0,0), summary="test2")
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':_('DECLINED') }, self.jack['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
         self.assertEqual(response, None, "No reply expected")
 
         event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
@@ -494,7 +506,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,14, 9,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.send_itip_invitation(self.jane['mail'], start)
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -507,7 +519,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         new_start = datetime.datetime(2014,8,15, 15,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         self.send_itip_update(self.jane['mail'], uid, new_start, summary="test", sequence=1)
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -523,7 +535,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.send_itip_invitation(self.jack['mail'], start)
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         # send update with new but conflicting date and incremented sequence
@@ -531,7 +543,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         new_start = datetime.datetime(2014,8,10, 9,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
         self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1)
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('DECLINED') }, self.jack['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
         self.assertEqual(response, None)
 
         # verify re-scheduled copy in jack's calendar with NEEDS-ACTION
@@ -577,7 +589,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled")
 
-        response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':_('ACCEPTED') }, self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
@@ -594,13 +606,19 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.purge_mailbox(self.john['mailbox'])
 
         start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
-        uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack])
+        uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.bob, self.jack])
 
         # send a reply from jane to john
         self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
 
         # check for notification message
-        # TODO: this notification should be suppressed until jack has replied, too
+        # this notification should be suppressed until bob has replied, too
+        notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+        self.assertEqual(notification, None)
+
+        # send a reply from bob to john
+        self.send_itip_reply(uid, self.bob['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+
         notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
         self.assertIsInstance(notification, email.message.Message)
 
@@ -610,14 +628,14 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         self.purge_mailbox(self.john['mailbox'])
 
-        # send a reply from jack to john
-        self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE')
+        # send a reply from bob to john
+        self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
 
+        # this triggers an additional notification
         notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
         self.assertIsInstance(notification, email.message.Message)
 
         notification_text = str(notification.get_payload());
-        self.assertIn(self.jack['mail'], notification_text)
         self.assertNotIn(_("PENDING"), notification_text)
 
 
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index 0b64f6a..dbe0713 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -142,4 +142,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         MIP.remove_write_lock(lock_key)
         self.assertFalse(os.path.isfile(lock_file))
 
+    def test_005_is_auto_reply(self):
+        all_manual  = [ 'ACT_MANUAL' ]
+        accept_none = [ 'ACT_REJECT' ]
+        accept_all  = [ 'ACT_ACCEPT', 'ACT_UPDATE' ]
+        accept_cond = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT' ]
+        accept_some = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_SAVE_TO_CALENDAR:example.org', 'ACT_REJECT_IF_CONFLICT' ]
+        accept_avail = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT:example.org' ]
+
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual },   'domain.org'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none },  'domain.org'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all },   'domain.com'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond },  'domain.com'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'domain.com'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'example.org'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'domain.com'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'example.org'))
         
\ No newline at end of file
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 1426e2b..03585ee 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,6 +41,7 @@ from pykolab.conf import Conf
 from pykolab.imap import IMAP
 from pykolab.xml import to_dt
 from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
 from pykolab.itip import events_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.itip import send_reply
@@ -500,12 +501,17 @@ def user_dn_from_email_address(email_address):
         auth = Auth()
         auth.connect()
 
+    # return cached value
+    if user_dn_from_email_address.cache.has_key(email_address):
+        return user_dn_from_email_address.cache[email_address]
+
     local_domains = auth.list_domains()
 
     if not local_domains == None:
         local_domains = list(set(local_domains.keys()))
 
     if not email_address.split('@')[1] in local_domains:
+        user_dn_from_email_address.cache[email_address] = None
         return None
 
     log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8)
@@ -517,8 +523,13 @@ def user_dn_from_email_address(email_address):
     else:
         log.debug(_("No user record(s) found for %r") % (email_address), level=9)
 
+    # remember this lookup
+    user_dn_from_email_address.cache[email_address] = user_dn
+
     return user_dn
 
+user_dn_from_email_address.cache = {}
+
 
 def get_matching_invitation_policies(receiving_user, sender_domain):
     # get user's kolabInvitationPolicy settings
@@ -843,6 +854,8 @@ def send_reply_notification(event, receiving_user):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
+    global auth
+
     import smtplib
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
@@ -851,6 +864,13 @@ def send_reply_notification(event, receiving_user):
         event.uid, receiving_user['mail']
     ), level=8)
 
+    organizer = event.get_organizer()
+    orgemail = organizer.email()
+    orgname = organizer.name()
+    sender_domain = orgemail.split('@')[-1]
+
+    auto_replies_expected = 0
+    auto_replies_received = 0
     partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] }
     for attendee in event.get_attendees():
         parstat = attendee.get_participant_status(True)
@@ -859,13 +879,34 @@ def send_reply_notification(event, receiving_user):
         else:
             partstats['PENDING'].append(attendee.get_displayname())
 
-    # TODO: for every attendee, look-up its kolabinvitationpolicy and skip notification
-    # until we got replies from all automatically responding attendees
+        # look-up kolabinvitationpolicy for this attendee
+        if attendee.get_cutype() == kolabformat.CutypeResource:
+            resource_dns = auth.find_resource(attendee.get_email())
+            if isinstance(resource_dns, list):
+                attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+            else:
+                attendee_dn = resource_dns
+        else:
+            attendee_dn = user_dn_from_email_address(attendee.get_email())
+
+        if attendee_dn:
+            attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
+            if is_auto_reply(attendee_rec, sender_domain):
+                auto_replies_expected += 1
+                if not parstat == 'NEEDS-ACTION':
+                    auto_replies_received += 1
+
+    # skip notification until we got replies from all automatically responding attendees
+    if auto_replies_received < auto_replies_expected:
+        log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
+            auto_replies_received, auto_replies_expected
+        ), level=8)
+        return
 
     roundup = ''
     for status,attendees in partstats.iteritems():
         if len(attendees) > 0:
-            roundup += "\n" + _(status) + ":\n" + "\n".join(attendees) + "\n"
+            roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
 
     message_text = """
         The event '%(summary)s' at %(start)s has been updated in your calendar.
@@ -882,11 +923,6 @@ def send_reply_notification(event, receiving_user):
     msg['To'] = receiving_user['mail']
     msg['Date'] = formatdate(localtime=True)
     msg['Subject'] = _('"%s" has been updated') % (event.get_summary())
-
-    organizer = event.get_organizer()
-    orgemail = organizer.email()
-    orgname = organizer.name()
-
     msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
 
     smtp = smtplib.SMTP("localhost", 10027)
@@ -902,6 +938,36 @@ def send_reply_notification(event, receiving_user):
     smtp.quit()
 
 
+def is_auto_reply(user, sender_domain):
+    accept_available = False
+    accept_conflicts = False
+    for policy in get_matching_invitation_policies(user, sender_domain):
+        if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE):
+            if check_policy_condition(policy, True):
+                accept_available = True
+            if check_policy_condition(policy, False):
+                accept_conflicts = True
+
+        # we have both cases covered by a policy
+        if accept_available and accept_conflicts:
+            return True
+
+        # manual action reached
+        if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR):
+            return False
+
+    return False
+
+
+def check_policy_condition(policy, available):
+    condition_fulfilled = True
+    if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
+        condition_fulfilled = available
+    if policy & COND_IF_CONFLICT:
+        condition_fulfilled = not condition_fulfilled
+    return condition_fulfilled
+
+
 def propagate_changes_to_attendees_calendars(event):
     """
         Find and update copies of this event in all attendee's calendars


commit d13dd3849d09d1eaa4fdd906686cb1217cc23ad4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 10 05:06:42 2014 -0400

    Use localized participant status texts in resource replies

diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 79ceaf2..60b6587 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -8,7 +8,9 @@ import uuid
 from pykolab.imap import IMAP
 from wallace import module_resources
 
+from pykolab.translate import _
 from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
 from email import message_from_string
 from twisted.trial import unittest
 
@@ -185,6 +187,8 @@ class TestResourceInvitation(unittest.TestCase):
 
     @classmethod
     def setup_class(self, *args, **kw):
+        self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s")
+
         from tests.functional.purge_users import purge_users
         purge_users()
 
@@ -373,7 +377,7 @@ class TestResourceInvitation(unittest.TestCase):
     def test_002_invite_resource(self):
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 10,0,0))
 
-        response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -384,7 +388,7 @@ class TestResourceInvitation(unittest.TestCase):
     def test_003_invite_resource_conflict(self):
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0))
 
-        response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         self.assertEqual(self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid), None)
@@ -396,7 +400,7 @@ class TestResourceInvitation(unittest.TestCase):
         uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,7,13, 12,0,0))
 
         # one of the collection members accepted the reservation
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
 
         delegatee = self.find_resource_by_email(accept['from'])
@@ -406,7 +410,7 @@ class TestResourceInvitation(unittest.TestCase):
         self.assertIsInstance(self.check_resource_calendar_event(delegatee['kolabtargetfolder'], uid), pykolab.xml.Event)
 
         # resource collection responds with a DELEGATED message
-        response = self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail'])
         self.assertIsInstance(response, email.message.Message)
         self.assertIn("ROLE=NON-PARTICIPANT;RSVP=FALSE", str(response))
 
@@ -416,13 +420,13 @@ class TestResourceInvitation(unittest.TestCase):
 
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,4,1, 10,0,0))
 
-        response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         self.purge_mailbox(self.john['mailbox'])
         self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,4,1, 12,0,0)) # conflict with myself
 
-        response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -437,13 +441,13 @@ class TestResourceInvitation(unittest.TestCase):
         uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,4,24, 12,0,0))
 
         # one of the collection members accepted the reservation
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
         delegatee = self.find_resource_by_email(accept['from'])
 
         # book that resource for the next day
         self.send_itip_invitation(delegatee['mail'], datetime.datetime(2014,4,25, 14,0,0))
-        accept2 = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept2 = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
 
         # re-schedule first booking to a conflicting date
         self.purge_mailbox(self.john['mailbox'])
@@ -451,7 +455,7 @@ class TestResourceInvitation(unittest.TestCase):
         self.send_itip_update(delegatee['mail'], uid, datetime.datetime(2014,4,25, 12,0,0), template=update_template)
 
         # expect response from another member of the initially delegated collection
-        new_accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        new_accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(new_accept, email.message.Message)
 
         new_delegatee = self.find_resource_by_email(new_accept['from'])
@@ -462,7 +466,7 @@ class TestResourceInvitation(unittest.TestCase):
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # old resource responds with a DELEGATED message
-        response = self.check_message_received("Reservation Request for test was DELEGATED", delegatee['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, delegatee['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         # old reservation was removed from old delegate's calendar
@@ -483,7 +487,7 @@ class TestResourceInvitation(unittest.TestCase):
         # make new reservation to the now free'd slot
         self.send_itip_invitation(self.boxter['mail'], datetime.datetime(2014,5,1, 9,0,0))
 
-        response = self.check_message_received("Reservation Request for test was ACCEPTED", self.boxter['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.boxter['mail'])
         self.assertIsInstance(response, email.message.Message)
 
 
@@ -494,7 +498,7 @@ class TestResourceInvitation(unittest.TestCase):
         uid = self.send_itip_invitation(self.cars['mail'], dt)
 
         # wait for accept notification
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
         delegatee = self.find_resource_by_email(accept['from'])
 
@@ -505,12 +509,12 @@ class TestResourceInvitation(unittest.TestCase):
         self.send_itip_update(delegatee['mail'], uid, dt, template=update_template)
 
         # get response from delegatee
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
         self.assertIn(delegatee['mail'], accept['from'])
 
         # no delegation response on updates
-        self.assertEqual(self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail']), None)
+        self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail']), None)
 
 
     def test_008_allday_reservation(self):
@@ -518,7 +522,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2), True)
 
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
 
         event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -526,7 +530,7 @@ class TestResourceInvitation(unittest.TestCase):
         self.assertIsInstance(event.get_start(), datetime.date)
 
         uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2, 16,0,0))
-        response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
 
 
@@ -537,19 +541,19 @@ class TestResourceInvitation(unittest.TestCase):
         uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,20, 12,0,0),
             template=itip_recurring.replace(";COUNT=10", ""))
 
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
 
         # check non-recurring against recurring
         uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,13, 10,0,0))
-        response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         self.purge_mailbox(self.john['mailbox'])
 
         # check recurring against recurring
         uid3 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,22, 8,0,0), template=itip_recurring)
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         self.assertIsInstance(accept, email.message.Message)
 
 
@@ -564,7 +568,7 @@ class TestResourceInvitation(unittest.TestCase):
         itip_invalid = itip_invitation.replace("DTSTART;", "X-DTSTART;")
         self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,24, 19,30,0), template=itip_invalid)
 
-        self.assertEqual(self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']), None)
+        self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']), None)
 
 
     def test_011_owner_info(self):
@@ -572,7 +576,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,6,19, 16,0,0))
 
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'])
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room1['mail'])
         self.assertIsInstance(accept, email.message.Message)
         respose_text = str(accept.get_payload(0))
         self.assertIn(self.jane['mail'], respose_text)
@@ -584,7 +588,7 @@ class TestResourceInvitation(unittest.TestCase):
 
         self.send_itip_invitation(self.room2['mail'], datetime.datetime(2014,6,19, 16,0,0))
 
-        accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'])
+        accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room2['mail'])
         self.assertIsInstance(accept, email.message.Message)
         respose_text = str(accept.get_payload(0))
         self.assertIn(self.jane['mail'], respose_text)
@@ -598,12 +602,12 @@ class TestResourceInvitation(unittest.TestCase):
         self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,8,4, 13,0,0))
 
         # check notification message sent to resource owner (jane)
-        notify = self.check_message_received("Booking for %s has been ACCEPTED" % (self.room1['cn']), self.room1['mail'], self.jane['mailbox'])
+        notify = self.check_message_received(_('Booking for %s has been %s') % (self.room1['cn'], participant_status_label('ACCEPTED')), self.room1['mail'], self.jane['mailbox'])
         self.assertIsInstance(notify, email.message.Message)
 
         notification_text = str(notify.get_payload())
         self.assertIn(self.john['mail'], notification_text)
-        self.assertIn("ACCEPTED", notification_text)
+        self.assertIn(participant_status_label('ACCEPTED'), notification_text)
 
         self.purge_mailbox(self.john['mailbox'])
 
@@ -611,9 +615,9 @@ class TestResourceInvitation(unittest.TestCase):
         self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,8,4, 12,30,0))
 
         # one of the collection members accepted the reservation
-        accepted = self.check_message_received("Reservation Request for test was ACCEPTED")
+        accepted = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
         delegatee = self.find_resource_by_email(accepted['from'])
 
-        notify = self.check_message_received("Booking for %s has been ACCEPTED" % (delegatee['cn']), delegatee['mail'], self.jane['mailbox'])
+        notify = self.check_message_received(_('Booking for %s has been %s') % (delegatee['cn'], participant_status_label('ACCEPTED')), delegatee['mail'], self.jane['mailbox'])
         self.assertIsInstance(notify, email.message.Message)
         self.assertIn(self.john['mail'], notification_text)
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index b99cf1a..dba2653 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -42,6 +42,7 @@ from pykolab.conf import Conf
 from pykolab.imap import IMAP
 from pykolab.xml import to_dt
 from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
 from pykolab.itip import events_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.translate import _
@@ -905,7 +906,7 @@ def reservation_response_text(status, owner):
         *** This is an automated response, please do not reply! ***
         
         We hereby inform you that your reservation was %s.
-    """) % (_(status))
+    """) % (participant_status_label(status))
 
     if owner:
         message_text += _("""
@@ -950,7 +951,7 @@ def send_owner_notification(resource, owner, itip_event, success=True):
         msg['To'] = owner['mail']
         msg['From'] = resource['mail']
         msg['Date'] = formatdate(localtime=True)
-        msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], _(status) if success else _('failed'))
+        msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], participant_status_label(status) if success else _('failed'))
 
         smtp = smtplib.SMTP("localhost", 10027)
 
@@ -986,7 +987,7 @@ def owner_notification_text(resource, owner, event, success):
         'resource': resource['cn'],
         'summary': event.get_summary(),
         'date': event.get_date_text(),
-        'status': _(status),
+        'status': participant_status_label(status),
         'orgname': organizer.name(),
         'orgemail': organizer.email()
     }


commit a44dcfb0690305fe42f3b374cef0fbced465d64d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 10 05:00:39 2014 -0400

    Fix all-day event date checks

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 8cf6435..17da24e 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -3,6 +3,7 @@ import pykolab
 
 from pykolab.xml import to_dt
 from pykolab.xml import event_from_ical
+from pykolab.xml import participant_status_label
 from pykolab.translate import _
 
 log = pykolab.getLogger('pykolab.wallace')
@@ -144,7 +145,7 @@ def check_event_conflict(kolab_event, itip_event):
         return conflict
 
     _es = to_dt(kolab_event.get_start())
-    _ee = to_dt(kolab_event.get_end())
+    _ee = to_dt(kolab_event.get_ical_dtend())  # use iCal style end date: next day for all-day events
 
     # naive loops to check for collisions in (recurring) events
     # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday)
@@ -208,10 +209,10 @@ def send_reply(from_address, itip_events, response_text, subject=None):
         participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
 
         event_summary = itip_event['xml'].get_summary()
-        message_text = response_text % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
+        message_text = response_text % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
 
         if subject is not None:
-            subject = subject % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
+            subject = subject % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
 
         try:
             message = itip_event['xml'].to_message_itip(from_address,
diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py
index abbaa92..a120fd2 100644
--- a/tests/unit/test-011-itip.py
+++ b/tests/unit/test-011-itip.py
@@ -357,6 +357,12 @@ class TestITip(unittest.TestCase):
         event.set_uid(itip_event['uid'])
         self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict for same UID")
 
+        allday = Event()
+        allday.set_start(datetime.date(2012,7,13))
+        allday.set_end(datetime.date(2012,7,13))
+
+        self.assertTrue(itip.check_event_conflict(allday, itip_event), "Conflicting allday event")
+
         event2 = Event()
         event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central")))
         event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central")))


commit 214bde4656f3e3a10096ba54dfa2704f4e8de719
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 10 04:35:36 2014 -0400

    New function to get a localized string for iCal participant status

diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 64b06ae..2bb42d4 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -1,5 +1,6 @@
 from attendee import Attendee
 from attendee import InvalidAttendeeParticipantStatusError
+from attendee import participant_status_label
 
 from contact import Contact
 from contact_reference import ContactReference
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 579158e..220ab8c 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -4,6 +4,26 @@ from pykolab.translate import _
 
 from contact_reference import ContactReference
 
+participant_status_labels = {
+        "NEEDS-ACTION": _("Needs Action"),
+        "ACCEPTED": _("Accepted"),
+        "DECLINED": _("Declined"),
+        "TENTATIVE": _("Tentatively Accepted"),
+        "DELEGATED": _("Delegated"),
+        "COMPLETED": _("Completed"),
+        "IN-PROCESS": _("In Process"),
+        # support integer values, too
+        kolabformat.PartNeedsAction: _("Needs Action"),
+        kolabformat.PartAccepted: _("Accepted"),
+        kolabformat.PartDeclined: _("Declined"),
+        kolabformat.PartTentative: _("Tentatively Accepted"),
+        kolabformat.PartDelegated: _("Delegated"),
+    }
+
+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,
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 98a91aa..4ac4997 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -12,6 +12,7 @@ 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.translate import _
 
 from os import path
@@ -928,7 +929,7 @@ class Event(object):
         msg['Date'] = formatdate(localtime=True)
 
         if subject is None:
-            subject = _("Invitation for %s was %s") % (self.get_summary(), _(participant_status))
+            subject = _("Invitation for %s was %s") % (self.get_summary(), participant_status_label(participant_status))
 
         msg["Subject"] = subject
 
diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py
index 9da93c7..8bcee3c 100644
--- a/tests/unit/test-002-attendee.py
+++ b/tests/unit/test-002-attendee.py
@@ -1,7 +1,9 @@
 import datetime
 import unittest
+import kolabformat
 
 from pykolab.xml import Attendee
+from pykolab.xml import participant_status_label
 
 class TestEventXML(unittest.TestCase):
     attendee = Attendee("jane at doe.org")
@@ -101,5 +103,10 @@ class TestEventXML(unittest.TestCase):
         self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 2][0], "INDIVIDUAL")
         self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 3][0], "RESOURCE")
 
+    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")
+
 if __name__ == '__main__':
     unittest.main()


commit d1f4cc6e05eea2f7d3b9e0a92ab0a2c5d10e926f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 9 20:24:51 2014 -0400

    Refine invitationpolicy module's code and behavior on re-scheduling and updates

diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index b12b785..8dc3ff7 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -322,6 +322,35 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return event.get_uid()
 
+    def update_calendar_event(self, uid, start=None, summary=None, sequence=0, user=None):
+        if user is None:
+            user = self.john
+
+        event = self.check_user_calendar_event(user['kolabtargetfolder'], uid)
+        if event:
+            if start is not None:
+                event.set_start(start)
+            if summary is not None:
+                event.set_summary(summary)
+            if sequence is not None:
+                event.set_sequence(sequence)
+
+            imap = IMAP()
+            imap.connect()
+
+            mailbox = imap.folder_quote(user['kolabtargetfolder'])
+            imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+            imap.imap.m.select(mailbox)
+
+            return imap.imap.m.append(
+                mailbox,
+                None,
+                None,
+                event.to_message().as_string()
+            )
+
+        return False
+
     def check_message_received(self, subject, from_addr=None, mailbox=None):
         if mailbox is None:
             mailbox = self.john['mailbox']
@@ -641,3 +670,26 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
         self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
         self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+        # PART 2: create conflicting event in jack's calendar
+        new_start = datetime.datetime(2014,8,21, 6,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        self.create_calendar_event(new_start, user=self.jack, attendees=[], summary="blocker")
+
+        # re-schedule initial event to new date
+        self.update_calendar_event(uid, start=new_start, sequence=1, user=self.john)
+        self.send_itip_update(self.jane['mail'], uid, new_start, summary="test (updated)", sequence=1)
+        self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1)
+
+        # wait for replies to be processed and propagated
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+
+        # check updated event in organizer's calendar (jack didn't reply yet)
+        self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+        self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+        # check partstats in jack's calendar: jack's status should remain needs-action
+        jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+        self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index b7f59de..1426e2b 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -23,7 +23,7 @@ import tempfile
 import time
 from urlparse import urlparse
 import urllib
-import md5
+import hashlib
 
 from email import message_from_string
 from email.parser import Parser
@@ -235,7 +235,7 @@ def execute(*args, **kw):
         return filepath
 
     # we're looking at the first itip event object
-    itip_event = itip_events[0];
+    itip_event = itip_events[0]
 
     # for replies, the organizer is the recipient
     if itip_event['method'] == 'REPLY':
@@ -327,6 +327,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
     save_event = not nonpart or not partstat == kolabformat.PartNeedsAction
     rsvp = receiving_attendee.get_rsvp()
     scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction
+    respond_with = receiving_attendee.get_participant_status(True)
     condition_fulfilled = True
 
     # find existing event in user's calendar
@@ -335,7 +336,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
     # compare sequence number to determine a (re-)scheduling request
     if existing is not None:
         log.debug(_("Existing event: %r") % (existing), level=9)
-        scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] >= existing.get_sequence()
+        scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence()
         save_event = True
 
     # if scheduling: check availability
@@ -347,8 +348,6 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
 
         log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
 
-    # if RSVP, send an iTip REPLY
-    if rsvp or scheduling_required:
         respond_with = None
         if policy & ACT_ACCEPT and condition_fulfilled:
             respond_with = 'TENTATIVE' if policy & COND_TENTATIVE else 'ACCEPTED'
@@ -361,12 +360,14 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             # TODO: delegate (but to whom?)
             return None
 
+    # if RSVP, send an iTip REPLY
+    if rsvp or scheduling_required:
+        # set attendee's CN from LDAP record if yet missing
+        if not receiving_attendee.get_name() and receiving_user.has_key('cn'):
+            receiving_attendee.set_name(receiving_user['cn'])
+
         # send iTip reply
         if respond_with is not None:
-            # set attendee's CN from LDAP record if yet missing
-            if not receiving_attendee.get_name() and receiving_user.has_key('cn'):
-                receiving_attendee.set_name(receiving_user['cn'])
-
             receiving_attendee.set_participant_status(respond_with)
             send_reply(recipient_email, itip_event, invitation_response_text(),
                 subject=_('"%(summary)s" has been %(status)s'))
@@ -516,8 +517,6 @@ def user_dn_from_email_address(email_address):
     else:
         log.debug(_("No user record(s) found for %r") % (email_address), level=9)
 
-    auth.disconnect()
-
     return user_dn
 
 
@@ -766,7 +765,7 @@ def remove_write_lock(key, update=True):
 
 
 def get_lock_key(user, uid):
-    return md5.new("%s/%s" % (user['mail'], uid)).hexdigest()
+    return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest()
 
 
 def update_event(event, user_rec):
@@ -909,13 +908,26 @@ def propagate_changes_to_attendees_calendars(event):
     """
     for attendee in event.get_attendees():
         attendee_user_dn = user_dn_from_email_address(attendee.get_email())
-        if attendee_user_dn is not None:
-            log.debug(_("Update attendee copy of %r") % (attendee_user_dn), level=9)
-
+        if attendee_user_dn:
             attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
             attendee_event = find_existing_event(event.uid, attendee_user, True)  # does IMAP authenticate
             if attendee_event:
-                attendee_event.event.setAttendees(event.get_attendees())
+                try:
+                    attendee_entry = attendee_event.get_attendee_by_email(attendee_user['mail'])
+                except:
+                    attendee_entry = None
+
+                # copy all attendees from master event (covers additions and removals)
+                new_attendees = kolabformat.vectorattendee();
+                for a in event.get_attendees():
+                    # keep my own entry intact
+                    if attendee_entry is not None and attendee_entry.get_email() == a.get_email():
+                        new_attendees.append(attendee_entry)
+                    else:
+                        new_attendees.append(a)
+
+                attendee_event.event.setAttendees(new_attendees)
+
                 success = update_event(attendee_event, attendee_user)
                 log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], event.uid, success), level=8)
 


commit 33fc8d4ba377a22e9c4c5a61e22fd7d1e217ffa8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 9 19:23:52 2014 -0400

    Better date text for all-day events

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 8550714..98a91aa 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -302,11 +302,20 @@ class Event(object):
     def get_date_text(self, date_format='%Y-%m-%d', time_format='%H:%M %Z'):
         start = self.get_start()
         end = self.get_end()
-        if start.date() == end.date():
+        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_exception_dates(self):


commit 2e506e215f44a6df7299ee0a424931b95fd5062c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 9 17:55:18 2014 -0400

    Shift event end date by one day when converting all-day events from/to iCal

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index de9e4d9..8550714 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -419,7 +419,11 @@ class Event(object):
         return self.get_created()
 
     def get_ical_dtend(self):
-        return self.get_end()
+        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:
@@ -632,6 +636,9 @@ class Event(object):
                 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):
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 81337d9..2c5a478 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -1,3 +1,4 @@
+import re
 import datetime
 import pytz
 import sys
@@ -218,6 +219,25 @@ METHOD:REQUEST
         self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:])
         self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:])
 
+    def test_018_ical_allday_events(self):
+        ical = """BEGIN:VEVENT
+UID:ffffffff-f783-4b58-b404-b1389bd2ffff
+DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
+CREATED;VALUE=DATE-TIME:20140407T122245Z
+DTSTART;VALUE=DATE:20140823
+DTEND;VALUE=DATE:20140824
+SUMMARY:All day
+DESCRIPTION:One single day
+TRANSP:OPAQUE
+CLASS:PUBLIC
+END:VEVENT
+"""
+        event = event_from_ical(ical)
+        self.assertEqual(str(event.get_start()), "2014-08-23")
+        self.assertEqual(str(event.get_end()), "2014-08-23")
+        self.assertEqual(str(event.get_ical_dtend()), "2014-08-24")
+        self.assertTrue(re.match('.*<dtend>\s*<date>2014-08-23</date>', str(event), re.DOTALL))
+
     def test_019_as_string_itip(self):
         self.event.set_summary("test")
         self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))


commit 3e492389ad7f2d628134b3471bc2e4e054b7d204
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 9 16:52:32 2014 -0400

    Send iTip replies through wallace again; use a locking mechanism to sequencially process partstat updates from (automated) replies

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 43646df..8cf6435 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -224,7 +224,7 @@ def send_reply(from_address, itip_events, response_text, subject=None):
             log.error(_("Failed to compose iTip reply message: %r") % (e))
             return
 
-        smtp = smtplib.SMTP("localhost", 10027)
+        smtp = smtplib.SMTP("localhost", 10026)  # replies go through wallace again
 
         if conf.debuglevel > 8:
             smtp.set_debuglevel(True)
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 2b669ff..b12b785 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -623,13 +623,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.send_itip_invitation(self.jane['mail'], start, template=event_itip)
         self.send_itip_invitation(self.jack['mail'], start, template=event_itip)
 
-        # send replies from jack and jane
-        # FIXME: replies should not be necessary if auto-replies get through wallace as well
-        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
-        time.sleep(10)  # FIXME: implement locking in wallace
-        self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE')
-
-        # wait for replies to be processed and propagated
+        # wait for replies from jack and jane to be processed and propagated
         time.sleep(10)
         event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index 650879b..0b64f6a 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -1,6 +1,7 @@
+import os
 import pykolab
 import logging
-import datetime
+import time
 
 from icalendar import Calendar
 from email import message
@@ -72,11 +73,10 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
     def setUp(self):
         # monkey-patch the pykolab.auth module to check API calls
         # without actually connecting to LDAP
-        #self.patch(pykolab.auth.Auth, "connect", self._mock_nop)
-        #self.patch(pykolab.auth.Auth, "disconnect", self._mock_nop)
-        #self.patch(pykolab.auth.Auth, "find_user_dn", self._mock_find_user_dn)
-        #self.patch(pykolab.auth.Auth, "get_entry_attributes", self._mock_get_entry_attributes)
-        #self.patch(pykolab.auth.Auth, "search_entry_by_attribute", self._mock_search_entry_by_attribute)
+        self.patch(pykolab.auth.Auth, "connect", self._mock_nop)
+        self.patch(pykolab.auth.Auth, "disconnect", self._mock_nop)
+        self.patch(pykolab.auth.Auth, "find_user_dn", self._mock_find_user_dn)
+        self.patch(pykolab.auth.Auth, "get_entry_attributes", self._mock_get_entry_attributes)
 
         # intercept calls to smtplib.SMTP.sendmail()
         import smtplib
@@ -127,3 +127,19 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] }
         self.assertEqual(MIP.get_matching_invitation_policies(user, 'somedomain.net'), [MIP.ACT_MANUAL])
+
+    def test_004_write_locks(self):
+        user = { 'cn': 'John Doe', 'mail': "doe at example.org" }
+
+        lock_key = MIP.get_lock_key(user, '1234567890-abcdef')
+        lock_file = os.path.join(MIP.mybasepath, 'locks', lock_key + '.lock')
+        MIP.set_write_lock(lock_key)
+
+        time.sleep(1)
+        self.assertTrue(os.path.isfile(lock_file))
+        self.assertFalse(MIP.set_write_lock(lock_key, False))
+
+        MIP.remove_write_lock(lock_key)
+        self.assertFalse(os.path.isfile(lock_file))
+
+        
\ No newline at end of file
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index ec3ad44..b7f59de 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -23,6 +23,7 @@ import tempfile
 import time
 from urlparse import urlparse
 import urllib
+import md5
 
 from email import message_from_string
 from email.parser import Parser
@@ -92,6 +93,7 @@ mybasepath = '/var/spool/pykolab/wallace/invitationpolicy/'
 
 auth = None
 imap = None
+write_locks = []
 
 def __init__():
     modules.register('invitationpolicy', execute, description=description())
@@ -123,7 +125,7 @@ def description():
     return """Invitation policy execution module."""
 
 def cleanup():
-    global auth, imap
+    global auth, imap, write_locks
 
     log.debug("cleanup(): %r, %r" % (auth, imap), level=9)
 
@@ -134,13 +136,17 @@ def cleanup():
     imap.disconnect()
     del imap
 
+    # remove remaining write locks
+    for key in write_locks:
+        remove_write_lock(key, False)
+
 def execute(*args, **kw):
     global auth, imap
 
     if not os.path.isdir(mybasepath):
         os.makedirs(mybasepath)
 
-    for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]:
+    for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER', 'locks']:
         if not os.path.isdir(os.path.join(mybasepath, stage)):
             os.makedirs(os.path.join(mybasepath, stage))
 
@@ -149,9 +155,14 @@ def execute(*args, **kw):
     auth = Auth()
     imap = IMAP()
 
-    # TODO: Test for correct call.
     filepath = args[0]
 
+    # ignore calls on lock files
+    if '/locks/' in filepath or kw.has_key('stage') and kw['stage'] == 'locks':
+        return False
+
+    log.debug("Invitation policy executing for %r, %r" % (filepath, '/locks/' in filepath), level=8)
+
     if kw.has_key('stage'):
         log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8)
 
@@ -276,6 +287,9 @@ def execute(*args, **kw):
             if done is not None:
                 break
 
+            # remove possible write lock from this iteration
+            remove_write_lock(get_lock_key(receiving_user, itip_event['uid']))
+
     else:
         log.debug(_("Ignoring '%s' iTip method") % (itip_event['method']), level=8)
 
@@ -316,7 +330,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
     condition_fulfilled = True
 
     # find existing event in user's calendar
-    existing = find_existing_event(itip_event['uid'], receiving_user)
+    existing = find_existing_event(itip_event['uid'], receiving_user, True)
 
     # compare sequence number to determine a (re-)scheduling request
     if existing is not None:
@@ -407,7 +421,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
 
         # find existing event in user's calendar
         # TODO: set/check lock to avoid concurrent wallace processes trying to update the same event simultaneously
-        existing = find_existing_event(itip_event['uid'], receiving_user)
+        existing = find_existing_event(itip_event['uid'], receiving_user, True)
 
         if existing:
             # compare sequence number to avoid outdated replies?
@@ -415,6 +429,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
                 log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Forwarding to Inbox.") % (
                     itip_event['sequence'], existing.get_sequence()
                 ))
+                remove_write_lock(existing._lock_key)
                 return MESSAGE_FORWARD
 
             log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
@@ -424,6 +439,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
                 log.error("Could not find corresponding attende in organizer's event: %r" % (e))
 
                 # TODO: accept new participant if ACT_ACCEPT ?
+                remove_write_lock(existing._lock_key)
                 return MESSAGE_FORWARD
 
             # update the organizer's copy of the event
@@ -457,7 +473,7 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
     # auto-update the local copy with STATUS=CANCELLED
     if policy & ACT_UPDATE:
         # find existing event in user's calendar
-        existing = find_existing_event(itip_event['uid'], receiving_user)
+        existing = find_existing_event(itip_event['uid'], receiving_user, True)
 
         if existing:
             existing.set_status('CANCELLED')
@@ -615,12 +631,18 @@ def list_user_calendars(user_rec):
     return calendars
 
 
-def find_existing_event(uid, user_rec):
+def find_existing_event(uid, user_rec, lock=False):
     """
         Search user's calendar folders for the given event (by UID)
     """
     global imap
 
+    lock_key = None
+
+    if lock:
+        lock_key = get_lock_key(user_rec, uid)
+        set_write_lock(lock_key)
+
     event = None
     for folder in list_user_calendars(user_rec):
         log.debug(_("Searching folder %r for event %r") % (folder, uid), level=8)
@@ -633,6 +655,7 @@ def find_existing_event(uid, user_rec):
             try:
                 event = event_from_message(message_from_string(data[0][1]))
                 setattr(event, '_imap_folder', folder)
+                setattr(event, '_lock_key', lock_key)
             except Exception, e:
                 log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e))
                 continue
@@ -640,6 +663,9 @@ def find_existing_event(uid, user_rec):
             if event and event.uid == uid:
                 return event
 
+    if lock_key is not None:
+        remove_write_lock(lock_key)
+
     return event
 
 
@@ -691,15 +717,73 @@ def check_availability(itip_event, receiving_user):
     return not conflict
 
 
+def set_write_lock(key, wait=True):
+    """
+        Set a write-lock for the given key and wait if such a lock already exists
+    """
+    if not os.path.isdir(mybasepath):
+        os.makedirs(mybasepath)
+    if not os.path.isdir(os.path.join(mybasepath, 'locks')):
+        os.makedirs(os.path.join(mybasepath, 'locks'))
+
+    file = os.path.join(mybasepath, 'locks', key + '.lock')
+    locked = os.path.getmtime(file) if os.path.isfile(file) else 0
+    expired = time.time() - 300
+
+    # wait if file lock is in place
+    while locked and locked > expired:
+        if not wait:
+            return False
+
+        log.debug(_("%r is locked, waiting...") % (key), level=9)
+        time.sleep(0.5)
+        locked = os.path.getmtime(file) if os.path.isfile(file) else 0
+
+    # touch the file
+    if os.path.isfile(file):
+        os.utime(file, None)
+    else:
+        open(file, 'w').close()
+
+    # register active lock
+    write_locks.append(key)
+
+    return True
+
+
+def remove_write_lock(key, update=True):
+    """
+        Remove the lock file for the given key
+    """
+    global write_locks
+
+    if key is not None:
+        file = os.path.join(mybasepath, 'locks', key + '.lock')
+        if os.path.isfile(file):
+            os.remove(file)
+            if update:
+                write_locks = [k for k in write_locks if not k == key]
+
+
+def get_lock_key(user, uid):
+    return md5.new("%s/%s" % (user['mail'], uid)).hexdigest()
+
+
 def update_event(event, user_rec):
     """
         Update the given event in IMAP (i.e. delete + append)
     """
+    success = False
+
     if hasattr(event, '_imap_folder'):
         delete_event(event)
-        return store_event(event, user_rec, event._imap_folder)
+        success = store_event(event, user_rec, event._imap_folder)
 
-    return False
+        # remove write lock for this event
+        if hasattr(event, '_lock_key') and event._lock_key is not None:
+            remove_write_lock(event._lock_key)
+
+    return success
 
 
 def store_event(event, user_rec, targetfolder=None):
@@ -829,7 +913,7 @@ def propagate_changes_to_attendees_calendars(event):
             log.debug(_("Update attendee copy of %r") % (attendee_user_dn), level=9)
 
             attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
-            attendee_event = find_existing_event(event.uid, attendee_user)  # does IMAP authenticate
+            attendee_event = find_existing_event(event.uid, attendee_user, True)  # does IMAP authenticate
             if attendee_event:
                 attendee_event.event.setAttendees(event.get_attendees())
                 success = update_event(attendee_event, attendee_user)


commit 7977586fcff0eb783ff4f17c2aa0bd2a1de262aa
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 9 03:43:14 2014 -0400

    Don't send iTip replies if resource booking failed (event not saved in resource calendar)

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 5e07552..b99cf1a 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -526,7 +526,9 @@ def accept_reservation_request(itip_event, resource, delegator=None):
     )
 
     owner = get_resource_owner(resource)
-    send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner)
+
+    if saved:
+        send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner)
 
     if owner:
         send_owner_notification(resource, owner, itip_event, saved)


commit 5038b40a73f111293abff38a82f6bc1764d164ba
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 9 03:35:53 2014 -0400

    Send owner notifications for resource bookings (#3167)

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 65eb818..de9e4d9 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -299,6 +299,16 @@ class Event(object):
                 dt = self.get_start() + duration
         return dt
 
+    def get_date_text(self, date_format='%Y-%m-%d', time_format='%H:%M %Z'):
+        start = self.get_start()
+        end = self.get_end()
+        if start.date() == end.date():
+            end_format = time_format
+        else:
+            end_format = date_format + " " + time_format
+
+        return "%s - %s" % (start.strftime(date_format + " " + time_format), end.strftime(end_format))
+
     def get_exception_dates(self):
         return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates())
 
diff --git a/tests/functional/resource_func.py b/tests/functional/resource_func.py
index 43aca96..ac80360 100644
--- a/tests/functional/resource_func.py
+++ b/tests/functional/resource_func.py
@@ -4,7 +4,7 @@ from pykolab import wap_client
 
 conf = pykolab.getConf()
 
-def resource_add(type, cn, members=None, owner=None):
+def resource_add(type, cn, members=None, owner=None, **kw):
     if type == None or type == '':
         raise Exception
 
@@ -18,6 +18,8 @@ def resource_add(type, cn, members=None, owner=None):
         'owner': owner
     }
 
+    resource_details.update(kw)
+
     result = wap_client.authenticate(conf.get('ldap', 'bind_dn'), conf.get('ldap', 'bind_pw'), conf.get('kolab', 'primary_domain'))
 
     type_id = 0
diff --git a/tests/functional/test_wallace/test_005_resource_add.py b/tests/functional/test_wallace/test_005_resource_add.py
index 2de60fb..fc7f3ed 100644
--- a/tests/functional/test_wallace/test_005_resource_add.py
+++ b/tests/functional/test_wallace/test_005_resource_add.py
@@ -29,8 +29,8 @@ class TestResourceAdd(unittest.TestCase):
         funcs.purge_resources()
         self.audi = funcs.resource_add("car", "Audi A4")
         self.passat = funcs.resource_add("car", "VW Passat")
-        self.boxter = funcs.resource_add("car", "Porsche Boxter S")
-        self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ])
+        self.boxter = funcs.resource_add("car", "Porsche Boxter S", kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
+        self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ], kolabinvitationpolicy='ACT_ACCEPT')
 
         from tests.functional.synchronize import synchronize_once
         synchronize_once()
@@ -56,3 +56,16 @@ class TestResourceAdd(unittest.TestCase):
         attrs = auth.get_entry_attributes(None, self.cars['dn'], ['*'])
         self.assertIn('groupofuniquenames', attrs['objectclass'])
         self.assertEqual(len(attrs['uniquemember']), 3)
+        self.assertEqual(attrs['kolabinvitationpolicy'], 'ACT_ACCEPT')
+
+    def test_003_get_resource_records(self):
+        resource_dns = module_resources.resource_record_from_email_address(self.cars['mail'])
+        self.assertEqual(resource_dns[0], self.cars['dn'])
+
+        resources = module_resources.get_resource_records(resource_dns)
+        self.assertEqual(len(resources), 4)
+
+        # check for (inherited) kolabinvitationpolicy values (bitmasks)
+        self.assertEqual(resources[self.cars['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT])
+        self.assertEqual(resources[self.audi['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT])
+        self.assertEqual(resources[self.boxter['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT_AND_NOTIFY])
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index d9f2d41..79ceaf2 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -214,9 +214,9 @@ class TestResourceInvitation(unittest.TestCase):
         self.boxter = funcs.resource_add("car", "Porsche Boxter S")
         self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ])
 
-        self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'])
+        self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
         self.room2 = funcs.resource_add("confroom", "Conference Room B-222")
-        self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'])
+        self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
 
         time.sleep(1)
         from tests.functional.synchronize import synchronize_once
@@ -353,12 +353,10 @@ class TestResourceInvitation(unittest.TestCase):
 
     def find_resource_by_email(self, email):
         resource = None
-        if (email.find(self.audi['mail']) >= 0):
-            resource = self.audi
-        if (email.find(self.passat['mail']) >= 0):
-            resource = self.passat
-        if (email.find(self.boxter['mail']) >= 0):
-            resource = self.boxter
+        for r in [self.audi, self.passat, self.boxter, self.room1, self.room2]:
+            if (email.find(r['mail']) >= 0):
+                resource = r
+                break
         return resource
 
 
@@ -593,20 +591,29 @@ class TestResourceInvitation(unittest.TestCase):
         self.assertIn(self.jane['displayname'], respose_text)
 
 
-    def TODO_test_012_owner_notification(self):
+    def test_012_owner_notification(self):
         self.purge_mailbox(self.john['mailbox'])
         self.purge_mailbox(self.jane['mailbox'])
 
-        self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,5,4, 13,0,0))
+        self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,8,4, 13,0,0))
 
         # check notification message sent to resource owner (jane)
-        notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'], self.jane['mailbox'])
+        notify = self.check_message_received("Booking for %s has been ACCEPTED" % (self.room1['cn']), self.room1['mail'], self.jane['mailbox'])
         self.assertIsInstance(notify, email.message.Message)
-        self.assertEqual(notify['From'], self.room1['mail'])
-        self.assertEqual(notify['Cc'], self.jane['mail'])
+
+        notification_text = str(notify.get_payload())
+        self.assertIn(self.john['mail'], notification_text)
+        self.assertIn("ACCEPTED", notification_text)
+
+        self.purge_mailbox(self.john['mailbox'])
 
         # check notification sent to collection owner (jane)
-        self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,5,4, 12,30,0))
+        self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,8,4, 12,30,0))
+
+        # one of the collection members accepted the reservation
+        accepted = self.check_message_received("Reservation Request for test was ACCEPTED")
+        delegatee = self.find_resource_by_email(accepted['from'])
 
-        notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'], self.jane['mailbox'])
+        notify = self.check_message_received("Booking for %s has been ACCEPTED" % (delegatee['cn']), delegatee['mail'], self.jane['mailbox'])
         self.assertIsInstance(notify, email.message.Message)
+        self.assertIn(self.john['mail'], notification_text)
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 3864f7c..5e07552 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -46,6 +46,18 @@ from pykolab.itip import events_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.translate import _
 
+# define some contstants used in the code below
+COND_NOTIFY = 256
+ACT_MANUAL  = 1
+ACT_ACCEPT  = 2
+ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY
+
+policy_name_map = {
+    'ACT_MANUAL':            ACT_MANUAL,
+    'ACT_ACCEPT':            ACT_ACCEPT,
+    'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY
+}
+
 log = pykolab.getLogger('pykolab.wallace')
 conf = pykolab.getConf()
 
@@ -513,7 +525,11 @@ def accept_reservation_request(itip_event, resource, delegator=None):
         level=8
     )
 
-    send_response(delegator['mail'] if delegator else resource['mail'], itip_event, get_resource_owner(resource))
+    owner = get_resource_owner(resource)
+    send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner)
+
+    if owner:
+        send_owner_notification(resource, owner, itip_event, saved)
 
 
 def decline_reservation_request(itip_event, resource):
@@ -527,8 +543,12 @@ def decline_reservation_request(itip_event, resource):
         "DECLINED"
     )
 
+    owner = get_resource_owner(resource)
     send_response(resource['mail'], itip_event, get_resource_owner(resource))
 
+    if owner:
+        send_owner_notification(resource, owner, itip_event, True)
+
 
 def save_resource_event(itip_event, resource):
     """
@@ -749,21 +769,25 @@ def get_resource_records(resource_dns):
         #   If it is not, ...
         resource_attrs = auth.get_entry_attributes(None, resource_dn, ['*'])
         resource_attrs['dn'] = resource_dn
+        parse_kolabinvitationpolicy(resource_attrs)
+
         if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
             if resource_attrs.has_key('uniquemember'):
                 resources[resource_dn] = resource_attrs
                 for uniquemember in resource_attrs['uniquemember']:
-                    resource_attrs = auth.get_entry_attributes(
+                    member_attrs = auth.get_entry_attributes(
                             None,
                             uniquemember,
                             ['*']
                         )
 
-                    if 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
-                        resource_attrs['dn'] = uniquemember
-                        resources[uniquemember] = resource_attrs
+                    if 'kolabsharedfolder' in [x.lower() for x in member_attrs['objectclass']]:
+                        member_attrs['dn'] = uniquemember
+                        parse_kolabinvitationpolicy(member_attrs, resource_attrs)
+
+                        resources[uniquemember] = member_attrs
                         resources[uniquemember]['memberof'] = resource_dn
-                        if not resource_attrs.has_key('owner') and resources[resource_dn].has_key('owner'):
+                        if not member_attrs.has_key('owner') and resources[resource_dn].has_key('owner'):
                             resources[uniquemember]['owner'] = resources[resource_dn]['owner']
                         resource_dns.append(uniquemember)
         else:
@@ -772,6 +796,16 @@ def get_resource_records(resource_dns):
     return resources
 
 
+def parse_kolabinvitationpolicy(attrs, parent=None):
+    if attrs.has_key('kolabinvitationpolicy'):
+        if not isinstance(attrs['kolabinvitationpolicy'], list):
+            attrs['kolabinvitationpolicy'] = [attrs['kolabinvitationpolicy']]
+        attrs['kolabinvitationpolicy'] = [policy_name_map[p] for p in attrs['kolabinvitationpolicy'] if policy_name_map.has_key(p)]
+
+    elif isinstance(parent, dict) and parent.has_key('kolabinvitationpolicy'):
+        attrs['kolabinvitationpolicy'] = parent['kolabinvitationpolicy']
+
+
 def get_resource_collection(email_address):
     """
         
@@ -878,3 +912,79 @@ def reservation_response_text(status, owner):
         """) % (owner['cn'], owner['mail'], owner['telephoneNumber'] if owner.has_key('telephoneNumber') else '')
     
     return message_text
+
+
+def send_owner_notification(resource, owner, itip_event, success=True):
+    """
+        Send a reservation notification to the resource owner
+    """
+    import smtplib
+    from pykolab import utils
+    from email.MIMEText import MIMEText
+    from email.Utils import formatdate
+
+    notify = False
+    status = itip_event['xml'].get_attendee_by_email(resource['mail']).get_participant_status(True)
+
+    if resource.has_key('kolabinvitationpolicy'):
+        for policy in resource['kolabinvitationpolicy']:
+            # 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
+        )
+
+        message_text = owner_notification_text(resource, owner, itip_event['xml'], success)
+
+        msg = MIMEText(utils.stripped_message(message_text))
+
+        msg['To'] = owner['mail']
+        msg['From'] = resource['mail']
+        msg['Date'] = formatdate(localtime=True)
+        msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], _(status) if success else _('failed'))
+
+        smtp = smtplib.SMTP("localhost", 10027)
+
+        if conf.debuglevel > 8:
+            smtp.set_debuglevel(True)
+
+        try:
+            smtp.sendmail(resource['mail'], owner['mail'], msg.as_string())
+        except Exception, e:
+            log.error(_("SMTP sendmail error: %r") % (e))
+
+        smtp.quit()
+
+def owner_notification_text(resource, owner, event, success):
+    organizer = event.get_organizer()
+    status = event.get_attendee_by_email(resource['mail']).get_participant_status(True)
+
+    if success:
+        message_text = _("""
+            The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s.
+
+            *** This is an automated message, sent to you as the resource owner. ***
+        """)
+    else:
+        message_text = _("""
+            A reservation request for %(resource)s could not be processed automatically.
+            Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s.
+
+            *** This is an automated message, sent to you as the resource owner. ***
+        """)
+
+    return message_text % {
+        'resource': resource['cn'],
+        'summary': event.get_summary(),
+        'date': event.get_date_text(),
+        'status': _(status),
+        'orgname': organizer.name(),
+        'orgemail': organizer.email()
+    }


commit ebcc6748bf105fda1183bce3242260b867271809
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 21:36:59 2014 -0400

    Simplify code, get rid of exec() calls; allow to set RSVP flag wen updtading a participant's status

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index f9b6487..65eb818 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -126,29 +126,31 @@ class Event(object):
 
         # NOTE: Make sure to list(set()) or duplicates may arise
         for attr in list(set(event.singletons)):
-            if hasattr(self, 'get_ical_%s' % (attr.lower())):
-                exec("retval = self.get_ical_%s()" % (attr.lower()))
+            ical_getter = 'get_ical_%s' % (attr.lower())
+            default_getter = 'get_%s' % (attr.lower())
+            retval = None
+            if hasattr(self, ical_getter):
+                retval = getattr(self, ical_getter)()
                 if not retval == None and not retval == "":
                     event.add(attr.lower(), retval)
-
-            elif hasattr(self, 'get_%s' % (attr.lower())):
-                exec("retval = self.get_%s()" % (attr.lower()))
+            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)):
-            if hasattr(self, 'get_ical_%s' % (attr.lower())):
-                exec("retval = self.get_ical_%s()" % (attr.lower()))
-                if isinstance(retval, list) and not len(retval) == 0:
-                    for _retval in retval:
-                        event.add(attr.lower(), _retval, encode=0)
-
-            elif hasattr(self, 'get_%s' % (attr.lower())):
-                exec("retval = self.get_%s()" % (attr.lower()))
-                if isinstance(retval, list) and not len(retval) == 0:
-                    for _retval in retval:
-                        event.add(attr.lower(), _retval, encode=0)
+            ical_getter = 'get_ical_%s' % (attr.lower())
+            default_getter = 'get_%s' % (attr.lower())
+            retval = None
+            if hasattr(self, ical_getter):
+                retval = getattr(self, ical_getter)()
+            elif hasattr(self, default_getter):
+                retval = getattr(self, default_getter)()
+
+            if isinstance(retval, list) and not len(retval) == 0:
+                for _retval in retval:
+                    event.add(attr.lower(), _retval, encode=0)
 
         cal.add_component(event)
 
@@ -491,7 +493,7 @@ class Event(object):
     def get_transparency(self):
         return self.event.transparency()
 
-    def set_attendee_participant_status(self, attendee, status):
+    def set_attendee_participant_status(self, attendee, status, rsvp=None):
         """
             Set the participant status of an attendee to status.
 
@@ -500,8 +502,11 @@ class Event(object):
             attendees for this event.
         """
         attendee = self.get_attendee(attendee)
-
         attendee.set_participant_status(status)
+
+        if rsvp is not None:
+            attendee.set_rsvp(rsvp)
+
         self.event.setAttendees(self._attendees)
 
     def set_status(self, status):
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 9488dd1..ec3ad44 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -419,7 +419,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
 
             log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
             try:
-                existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), False)
+                existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), rsvp=False)
             except Exception, e:
                 log.error("Could not find corresponding attende in organizer's event: %r" % (e))
 


commit e6ee15781cf3a3a8d0606351683438a5281e6530
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 21:34:38 2014 -0400

    Implement participant status updates propagated to all attendee's calendars

diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 0d2875c..2b669ff 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -177,6 +177,11 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
         }
 
+        self.external = {
+            'displayname': 'Bob External',
+            'mail': 'bob.external at gmail.com'
+        }
+
         from tests.functional.user_add import user_add
         user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage'])
         user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage'])
@@ -239,7 +244,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def send_itip_reply(self, uid, attendee_email, mailto, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'):
+    def send_itip_reply(self, uid, attendee_email, mailto, start=None, template=None, summary="test", sequence=0, partstat='ACCEPTED'):
         if start is None:
             start = datetime.datetime.now()
 
@@ -586,3 +591,59 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIn(self.jack['mail'], notification_text)
         self.assertNotIn(_("PENDING"), notification_text)
 
+
+    def test_009_outdated_reply(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        start = datetime.datetime(2014,9,2, 11,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.john, sequence=2)
+
+        # send a reply from jane to john
+        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start, sequence=1)
+
+        # verify jane's attendee status was not updated
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_sequence(), 2)
+        self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+
+
+    def test_010_partstat_update_propagation(self):
+        # ATTENTION: this test requires wallace.invitationpolicy_autoupdate_other_attendees_on_reply to be enabled in config
+
+        start = datetime.datetime(2014,8,21, 13,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack, self.external])
+
+        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+
+        # send invitations to jack and jane
+        event_itip = event.as_string_itip()
+        self.send_itip_invitation(self.jane['mail'], start, template=event_itip)
+        self.send_itip_invitation(self.jack['mail'], start, template=event_itip)
+
+        # send replies from jack and jane
+        # FIXME: replies should not be necessary if auto-replies get through wallace as well
+        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+        time.sleep(10)  # FIXME: implement locking in wallace
+        self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE')
+
+        # wait for replies to be processed and propagated
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+
+        # check updated event in organizer's calendar
+        self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+        self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+        # check updated partstats in jane's calendar
+        janes = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertEqual(janes.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+        self.assertEqual(janes.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+        # check updated partstats in jack's calendar
+        jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+        self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 41917da..9488dd1 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -316,7 +316,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
     condition_fulfilled = True
 
     # find existing event in user's calendar
-    existing = find_existing_event(itip_event, receiving_user)
+    existing = find_existing_event(itip_event['uid'], receiving_user)
 
     # compare sequence number to determine a (re-)scheduling request
     if existing is not None:
@@ -406,14 +406,20 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
             return MESSAGE_FORWARD
 
         # find existing event in user's calendar
-        existing = find_existing_event(itip_event, receiving_user)
+        # TODO: set/check lock to avoid concurrent wallace processes trying to update the same event simultaneously
+        existing = find_existing_event(itip_event['uid'], receiving_user)
 
         if existing:
-            log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
+            # compare sequence number to avoid outdated replies?
+            if not itip_event['sequence'] == existing.get_sequence():
+                log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Forwarding to Inbox.") % (
+                    itip_event['sequence'], existing.get_sequence()
+                ))
+                return MESSAGE_FORWARD
 
-            # TODO: compare sequence number to avoid outdated replies?
+            log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
             try:
-                existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status())
+                existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), False)
             except Exception, e:
                 log.error("Could not find corresponding attende in organizer's event: %r" % (e))
 
@@ -425,7 +431,10 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
                 if policy & COND_NOTIFY:
                     send_reply_notification(existing, receiving_user)
 
-                # TODO: update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
+                # update all other attendee's copies
+                if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
+                    propagate_changes_to_attendees_calendars(existing)
+
                 return MESSAGE_PROCESSED
 
         else:
@@ -448,7 +457,7 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
     # auto-update the local copy with STATUS=CANCELLED
     if policy & ACT_UPDATE:
         # find existing event in user's calendar
-        existing = find_existing_event(itip_event, receiving_user)
+        existing = find_existing_event(itip_event['uid'], receiving_user)
 
         if existing:
             existing.set_status('CANCELLED')
@@ -606,7 +615,7 @@ def list_user_calendars(user_rec):
     return calendars
 
 
-def find_existing_event(itip_event, user_rec):
+def find_existing_event(uid, user_rec):
     """
         Search user's calendar folders for the given event (by UID)
     """
@@ -614,10 +623,10 @@ def find_existing_event(itip_event, user_rec):
 
     event = None
     for folder in list_user_calendars(user_rec):
-        log.debug(_("Searching folder %r for event %r") % (folder, itip_event['uid']), level=8)
+        log.debug(_("Searching folder %r for event %r") % (folder, uid), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
-        typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (itip_event['uid']))
+        typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
         for num in reversed(data[0].split()):
             typ, data = imap.imap.m.fetch(num, '(RFC822)')
 
@@ -628,7 +637,7 @@ def find_existing_event(itip_event, user_rec):
                 log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e))
                 continue
 
-            if event and event.uid == itip_event['uid']:
+            if event and event.uid == uid:
                 return event
 
     return event
@@ -660,7 +669,6 @@ def check_availability(itip_event, receiving_user):
 
             try:
                 event = event_from_message(message_from_string(data[0][1]))
-                setattr(event, '_imap_folder', folder)
             except Exception, e:
                 log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e))
                 continue
@@ -811,6 +819,29 @@ def send_reply_notification(event, receiving_user):
     smtp.quit()
 
 
+def propagate_changes_to_attendees_calendars(event):
+    """
+        Find and update copies of this event in all attendee's calendars
+    """
+    for attendee in event.get_attendees():
+        attendee_user_dn = user_dn_from_email_address(attendee.get_email())
+        if attendee_user_dn is not None:
+            log.debug(_("Update attendee copy of %r") % (attendee_user_dn), level=9)
+
+            attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
+            attendee_event = find_existing_event(event.uid, attendee_user)  # does IMAP authenticate
+            if attendee_event:
+                attendee_event.event.setAttendees(event.get_attendees())
+                success = update_event(attendee_event, attendee_user)
+                log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], event.uid, success), level=8)
+
+            else:
+                log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], event.uid), level=8)
+
+        else:
+            log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8)
+
+
 def invitation_response_text():
     return _("""
         %(name)s has %(status)s your invitation for %(summary)s.


commit 317c074fc70c972ed068c01e658925512abbc28a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 19:34:07 2014 -0400

    Catch potential exceptions while sending iTip replies; set recipient parstat=needs-action when saving new/re-scheduled invitations directly to calendar

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index f32cad4..43646df 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -197,13 +197,9 @@ def send_reply(from_address, itip_events, response_text, subject=None):
     """
 
     import smtplib
-    smtp = smtplib.SMTP("localhost", 10027)
 
     conf = pykolab.getConf()
 
-    if conf.debuglevel > 8:
-        smtp.set_debuglevel(True)
-
     if isinstance(itip_events, dict):
         itip_events = [ itip_events ]
 
@@ -217,12 +213,25 @@ def send_reply(from_address, itip_events, response_text, subject=None):
         if subject is not None:
             subject = subject % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
 
-        message = itip_event['xml'].to_message_itip(from_address,
-            method="REPLY",
-            participant_status=participant_status,
-            message_text=message_text,
-            subject=subject
-        )
-        smtp.sendmail(message['From'], message['To'], message.as_string())
+        try:
+            message = itip_event['xml'].to_message_itip(from_address,
+                method="REPLY",
+                participant_status=participant_status,
+                message_text=message_text,
+                subject=subject
+            )
+        except Exception, e:
+            log.error(_("Failed to compose iTip reply message: %r") % (e))
+            return
+
+        smtp = smtplib.SMTP("localhost", 10027)
+
+        if conf.debuglevel > 8:
+            smtp.set_debuglevel(True)
+
+        try:
+            smtp.sendmail(message['From'], message['To'], message.as_string())
+        except Exception, e:
+            log.error(_("SMTP sendmail error: %r") % (e))
 
     smtp.quit()
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 3b68aef..0d2875c 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -150,7 +150,6 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.john = {
             'displayname': 'John Doe',
             'mail': 'john.doe at example.org',
-            'sender': 'John Doe <john.doe at example.org>',
             'dn': 'uid=doe,ou=People,dc=example,dc=org',
             'preferredlanguage': 'en_US',
             'mailbox': 'user/john.doe at example.org',
@@ -161,7 +160,6 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.jane = {
             'displayname': 'Jane Manager',
             'mail': 'jane.manager at example.org',
-            'sender': 'Jane Manager <jane.manager at example.org>',
             'dn': 'uid=manager,ou=People,dc=example,dc=org',
             'preferredlanguage': 'en_US',
             'mailbox': 'user/jane.manager at example.org',
@@ -172,7 +170,6 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.jack = {
             'displayname': 'Jack Tentative',
             'mail': 'jack.tentative at example.org',
-            'sender': 'Jack Tentative <jack.tentative at example.org>',
             'dn': 'uid=tentative,ou=People,dc=example,dc=org',
             'preferredlanguage': 'en_US',
             'mailbox': 'user/jack.tentative at example.org',
@@ -486,7 +483,32 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
 
     def test_005_invite_rescheduling_reject(self):
-        pass
+        self.purge_mailbox(self.john['mailbox'])
+        self.purge_mailbox(self.jack['kolabtargetfolder'])
+
+        start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.send_itip_invitation(self.jack['mail'], start)
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        # send update with new but conflicting date and incremented sequence
+        self.create_calendar_event(datetime.datetime(2014,8,10, 10,30,0, tzinfo=pytz.timezone("Europe/Berlin")), user=self.jack)
+        new_start = datetime.datetime(2014,8,10, 9,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1)
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('DECLINED') }, self.jack['mail'])
+        self.assertEqual(response, None)
+
+        # verify re-scheduled copy in jack's calendar with NEEDS-ACTION
+        event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_start(), new_start)
+        self.assertEqual(event.get_sequence(), 1)
+
+        attendee = event.get_attendee(self.jack['mail'])
+        self.assertTrue(attendee.get_rsvp())
+        self.assertEqual(attendee.get_participant_status(), kolabformat.PartNeedsAction)
 
 
     def test_006_invitation_reply(self):
@@ -563,3 +585,4 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         notification_text = str(notification.get_payload());
         self.assertIn(self.jack['mail'], notification_text)
         self.assertNotIn(_("PENDING"), notification_text)
+
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 7375d2d..41917da 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -283,10 +283,11 @@ def execute(*args, **kw):
     if done == MESSAGE_PROCESSED:
         log.debug(_("iTip message %r consumed by the invitationpolicy module") % (message.get('Message-ID')), level=5)
         os.unlink(filepath)
-        filepath = None
+        cleanup()
+        return None
 
-    cleanup()
-    return filepath
+    # accept message into the destination inbox
+    accept(filepath)
 
 
 def process_itip_request(itip_event, policy, recipient_email, sender_email, receiving_user):
@@ -357,7 +358,8 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
                 subject=_('"%(summary)s" has been %(status)s'))
 
         elif policy & ACT_SAVE_TO_CALENDAR:
-            # copy the invitation into the user's calendar with unchanged PARTSTAT
+            # copy the invitation into the user's calendar with PARTSTAT=NEEDS-ACTION
+            itip_event['xml'].set_attendee_participant_status(receiving_attendee, 'NEEDS-ACTION')
             save_event = True
 
         else:
@@ -801,7 +803,11 @@ def send_reply_notification(event, receiving_user):
     if conf.debuglevel > 8:
         smtp.set_debuglevel(True)
 
-    smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    try:
+        smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    except Exception, e:
+        log.error(_("SMTP sendmail error: %r") % (e))
+
     smtp.quit()
 
 


commit a674289628249f45ec46c3096b1c476692943ff3
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 18:43:25 2014 -0400

    Use new pykolab.xml.event_from_message() function in wallace modules and verify that attachments survive event updates

diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 946cb5f..d9f2d41 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -8,6 +8,7 @@ import uuid
 from pykolab.imap import IMAP
 from wallace import module_resources
 
+from pykolab.xml import event_from_message
 from email import message_from_string
 from twisted.trial import unittest
 
@@ -23,7 +24,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:%s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
 DTSTART;TZID=Europe/London:%s
 DTEND;TZID=Europe/London:%s
 SUMMARY:test
@@ -43,7 +44,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:%s
-DTSTAMP:20140215T1254140
+DTSTAMP:20140215T125414Z
 DTSTART;TZID=Europe/London:%s
 DTEND;TZID=Europe/London:%s
 SEQUENCE:2
@@ -90,7 +91,7 @@ CALSCALE:GREGORIAN
 METHOD:CANCEL
 BEGIN:VEVENT
 UID:%s
-DTSTAMP:20140218T1254140
+DTSTAMP:20140218T125414Z
 DTSTART;TZID=Europe/London:20120713T100000
 DTEND;TZID=Europe/London:20120713T110000
 SUMMARY:test
@@ -112,7 +113,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:%s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
 DTSTART;VALUE=DATE:%s
 DTEND;VALUE=DATE:%s
 SUMMARY:test
@@ -133,7 +134,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:%s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
 DTSTART;TZID=Europe/Zurich:%s
 DTEND;TZID=Europe/Zurich:%s
 RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
@@ -328,12 +329,7 @@ class TestResourceInvitation(unittest.TestCase):
                 if uid and event_message['subject'] != uid:
                     continue
 
-                for part in event_message.walk():
-                    if part.get_content_type() == "application/calendar+xml":
-                        payload = part.get_payload(decode=True)
-                        found = pykolab.xml.event_from_string(payload)
-                        break
-
+                found = event_from_message(event_message)
                 if found:
                     break
 
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 4fd61a7..3b68aef 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -11,6 +11,7 @@ from pykolab.imap import IMAP
 from wallace import module_resources
 
 from pykolab.translate import _
+from pykolab.xml import event_from_message
 from email import message_from_string
 from twisted.trial import unittest
 
@@ -26,7 +27,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:%(uid)s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
 DTSTART;TZID=Europe/Berlin:%(start)s
 DTEND;TZID=Europe/Berlin:%(end)s
 SUMMARY:%(summary)s
@@ -48,7 +49,7 @@ CALSCALE:GREGORIAN
 METHOD:CANCEL
 BEGIN:VEVENT
 UID:%(uid)s
-DTSTAMP:20140218T1254140
+DTSTAMP:20140218T125414Z
 DTSTART;TZID=Europe/Berlin:20120713T100000
 DTEND;TZID=Europe/Berlin:20120713T110000
 SUMMARY:%(summary)s
@@ -69,7 +70,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:%(uid)s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
 DTSTART;TZID=Europe/Zurich:%(start)s
 DTEND;TZID=Europe/Zurich:%(end)s
 RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
@@ -295,6 +296,14 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         event.set_summary(summary)
         event.set_sequence(sequence)
 
+        # create event with attachment
+        vattach = event.get_attachments()
+        attachment = kolabformat.Attachment()
+        attachment.setLabel('attach.txt')
+        attachment.setData('This is a text attachment', 'text/plain')
+        vattach.append(attachment)
+        event.event.setAttachments(vattach)
+
         imap = IMAP()
         imap.connect()
 
@@ -365,12 +374,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
                 if uid and event_message['subject'] != uid:
                     continue
 
-                for part in event_message.walk():
-                    if part.get_content_type() == "application/calendar+xml":
-                        payload = part.get_payload(decode=True)
-                        found = pykolab.xml.event_from_string(payload)
-                        break
-
+                found = event_from_message(event_message)
                 if found:
                     break
 
@@ -506,6 +510,11 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIsInstance(attendee, pykolab.xml.Attendee)
         self.assertEqual(attendee.get_participant_status(), kolabformat.PartAccepted)
 
+        # check attachments in update event
+        attachments = event.get_attachments()
+        self.assertEqual(len(attachments), 1)
+        self.assertEqual(event.get_attachment_data(0), 'This is a text attachment')
+
 
     def test_007_invitation_cancel(self):
         self.purge_mailbox(self.john['mailbox'])
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index a141251..7375d2d 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -39,7 +39,7 @@ from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
 from pykolab.xml import to_dt
-from pykolab.xml import event_from_string
+from pykolab.xml import event_from_message
 from pykolab.itip import events_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.itip import send_reply
@@ -619,15 +619,12 @@ def find_existing_event(itip_event, user_rec):
         for num in reversed(data[0].split()):
             typ, data = imap.imap.m.fetch(num, '(RFC822)')
 
-            event_message = message_from_string(data[0][1])
-
-            if event_message.is_multipart():
-                for part in event_message.walk():
-                    if part.get_content_type() == "application/calendar+xml":
-                        payload = part.get_payload(decode=True)
-                        event = event_from_string(payload)
-                        setattr(event, '_imap_folder', folder)
-                        break
+            try:
+                event = event_from_message(message_from_string(data[0][1]))
+                setattr(event, '_imap_folder', folder)
+            except Exception, e:
+                log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e))
+                continue
 
             if event and event.uid == itip_event['uid']:
                 return event
@@ -659,20 +656,18 @@ def check_availability(itip_event, receiving_user):
             event = None
             typ, data = imap.imap.m.fetch(num, '(RFC822)')
 
-            event_message = message_from_string(data[0][1])
-
-            if event_message.is_multipart():
-                for part in event_message.walk():
-                    if part.get_content_type() == "application/calendar+xml":
-                        payload = part.get_payload(decode=True)
-                        event = event_from_string(payload)
-                        break
-
-                if event and event.uid:
-                    conflict = check_event_conflict(event, itip_event)
-                    if conflict:
-                        log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid']))
-                        break
+            try:
+                event = event_from_message(message_from_string(data[0][1]))
+                setattr(event, '_imap_folder', folder)
+            except Exception, e:
+                log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e))
+                continue
+
+            if event and event.uid:
+                conflict = check_event_conflict(event, itip_event)
+                if conflict:
+                    log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid']))
+                    break
 
         if conflict:
             break
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f398120..3864f7c 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -40,8 +40,8 @@ import kolabformat
 from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
-from pykolab.xml import event_from_string
 from pykolab.xml import to_dt
+from pykolab.xml import event_from_message
 from pykolab.itip import events_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.translate import _
@@ -467,28 +467,29 @@ def read_resource_calendar(resource_rec, itip_events):
 
         event_message = message_from_string(data[0][1])
 
-        if event_message.is_multipart():
-            for part in event_message.walk():
-                if part.get_content_type() == "application/calendar+xml":
-                    payload = part.get_payload(decode=True)
-                    event = pykolab.xml.event_from_string(payload)
+        try:
+            event = event_from_message(message_from_string(data[0][1]))
+        except Exception, e:
+            log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e))
+            continue
 
-                    for itip in itip_events:
-                        conflict = check_event_conflict(event, itip)
+        if event:
+            for itip in itip_events:
+                conflict = check_event_conflict(event, itip)
 
-                        if event.get_uid() == itip['uid']:
-                            resource_rec['existing_events'].append(itip['uid'])
+                if event.get_uid() == itip['uid']:
+                    resource_rec['existing_events'].append(itip['uid'])
 
-                        if conflict:
-                            log.info(
-                                _("Event %r conflicts with event %r") % (
-                                    itip['xml'].get_uid(),
-                                    event.get_uid()
-                                )
-                            )
+                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
+                    resource_rec['conflicting_events'].append(event.get_uid())
+                    resource_rec['conflict'] = True
 
     return num_messages
 


commit 6e137a89f6f29975133633e2e70557b3954d4220
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 18:29:00 2014 -0400

    Add getter for event attachment data

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 39034f6..f9b6487 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -303,6 +303,22 @@ class Event(object):
     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()
 
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 363c75e..81337d9 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -389,6 +389,7 @@ METHOD:REQUEST
         self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:])
         self.assertEqual(parts[4].get_content_type(), 'text/plain')
         self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:])
+        self.assertEqual(event.get_attachment_data(1), 'This is a text file')
 
     def test_024_bogus_itip_data(self):
         # DTSTAMP contains an invalid date/time value


commit 5de26915e7b66db1dd139b1b6d33687f7e9dfc66
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 17:40:54 2014 -0400

    Fix event object reading and writing: store attachments as separate MIME parts and forward them to the new MIME message written

diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 5ca2837..64b06ae 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -9,6 +9,7 @@ from event import EventIntegrityError
 from event import InvalidEventDateError
 from event import event_from_ical
 from event import event_from_string
+from event import event_from_message
 
 from utils import to_dt
 
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index fcb3a17..39034f6 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -6,6 +6,7 @@ import kolabformat
 import pytz
 import time
 import uuid
+import base64
 
 import pykolab
 from pykolab import constants
@@ -13,6 +14,7 @@ from pykolab import utils
 from pykolab.xml import utils as xmlutils
 from pykolab.translate import _
 
+from os import path
 from attendee import Attendee
 from contact_reference import ContactReference
 
@@ -24,6 +26,21 @@ def event_from_ical(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):
     status_map = {
             "TENTATIVE": kolabformat.StatusTentative,
@@ -40,6 +57,7 @@ class Event(object):
     def __init__(self, from_ical="", from_string=""):
         self._attendees = []
         self._categories = []
+        self._attachment_parts = []
 
         if from_ical == "":
             if from_string == "":
@@ -751,15 +769,50 @@ class Event(object):
 
         msg["Subject"] = self.get_uid()
 
-        part.set_payload(str(self))
+        # 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'
 
-        # TODO: extract attachment data to separate MIME parts
+            (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):
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index f9ef92e..363c75e 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -12,6 +12,58 @@ from pykolab.xml import InvalidAttendeeParticipantStatusError
 from pykolab.xml import InvalidEventDateError
 from pykolab.xml import event_from_ical
 from pykolab.xml import event_from_string
+from pykolab.xml import event_from_message
+
+ical_event = """
+BEGIN:VEVENT
+UID:7a35527d-f783-4b58-b404-b1389bd2fc57
+DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
+CREATED;VALUE=DATE-TIME:20140407T122245Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z
+DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000
+DURATION:PT1H30M0S
+RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
+EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000
+EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000
+SUMMARY:Summary
+LOCATION:Location
+DESCRIPTION:Description\\n2 lines
+CATEGORIES:Personal
+TRANSP:OPAQUE
+PRIORITY:2
+SEQUENCE:2
+CLASS:PUBLIC
+ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP
+ E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager at example.org
+ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA
+ LSE:MAILTO:max at imum.com
+ORGANIZER;CN=Doe\, John:mailto:john.doe at example.org
+URL:http://somelink.com/foo
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn
+ g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i
+ ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF
+ xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX
+ V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS
+ bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U
+ eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV
+ m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg
+ lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d
+ ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q
+ QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3
+ ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo
+ ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf
+ vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr
+ tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/
+ Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg==
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh
+ pcyBpcyBhIHRleHQgZmlsZQo=
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-PT30M
+END:VALARM
+END:VEVENT
+"""
+
 
 class TestEventXML(unittest.TestCase):
     event = Event()
@@ -122,55 +174,8 @@ PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
   2.1.3//EN
 CALSCALE:GREGORIAN
 METHOD:REQUEST
-BEGIN:VEVENT
-UID:7a35527d-f783-4b58-b404-b1389bd2fc57
-DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
-CREATED;VALUE=DATE-TIME:20140407T122245Z
-LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z
-DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000
-DURATION:PT1H30M0S
-RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
-EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000
-EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000
-SUMMARY:Summary
-LOCATION:Location
-DESCRIPTION:Description\\n2 lines
-CATEGORIES:Personal
-TRANSP:OPAQUE
-PRIORITY:2
-SEQUENCE:2
-CLASS:PUBLIC
-ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP
- E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager at example.org
-ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA
- LSE:MAILTO:max at imum.com
-ORGANIZER;CN=Doe\, John:mailto:john.doe at example.org
-URL:http://somelink.com/foo
-ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn
- g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i
- ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF
- xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX
- V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS
- bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U
- eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV
- m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg
- lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d
- ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q
- QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3
- ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo
- ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf
- vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr
- tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/
- Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg==
-ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh
- pcyBpcyBhIHRleHQgZmlsZQo=
-BEGIN:VALARM
-ACTION:DISPLAY
-TRIGGER:-PT30M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-"""
+        """ + ical_event + "END:VCALENDAR"
+
         ical = icalendar.Calendar.from_ical(ical_str)
         event = event_from_ical(ical.walk('VEVENT')[0].to_ical())
 
@@ -193,6 +198,26 @@ END:VCALENDAR
         self.assertEqual(len(event.get_alarms()), 1)
         self.assertEqual(len(event.get_attachments()), 2)
 
+    def test_018_ical_to_message(self):
+        event = event_from_ical(ical_event)
+        message = event.to_message()
+
+        self.assertTrue(message.is_multipart())
+        self.assertEqual(message['Subject'], event.uid)
+        self.assertEqual(message['X-Kolab-Type'], 'application/x-vnd.kolab.event')
+
+        parts = [p for p in message.walk()]
+        attachments = event.get_attachments();
+
+        self.assertEqual(len(parts), 5)
+        self.assertEqual(parts[1].get_content_type(), 'text/plain')
+        self.assertEqual(parts[2].get_content_type(), 'application/calendar+xml')
+        self.assertEqual(parts[3].get_content_type(), 'image/png')
+        self.assertEqual(parts[4].get_content_type(), 'text/plain')
+        self.assertEqual(parts[2]['Content-ID'], None)
+        self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:])
+        self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:])
+
     def test_019_as_string_itip(self):
         self.event.set_summary("test")
         self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))
@@ -348,6 +373,44 @@ END:VCALENDAR
         self.assertIsInstance(event.get_start(), datetime.datetime)
         self.assertEqual(str(event.get_start()), "2014-08-13 10:00:00+00:00")
 
+    def test_023_load_from_message(self):
+        event = event_from_message(event_from_ical(ical_event).to_message())
+        event.set_sequence(3)
+
+        message = event.to_message()
+        self.assertTrue(message.is_multipart())
+
+        # check attachment MIME parts are kept
+        parts = [p for p in message.walk()]
+        attachments = event.get_attachments();
+
+        self.assertEqual(len(parts), 5)
+        self.assertEqual(parts[3].get_content_type(), 'image/png')
+        self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:])
+        self.assertEqual(parts[4].get_content_type(), 'text/plain')
+        self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:])
+
+    def test_024_bogus_itip_data(self):
+        # DTSTAMP contains an invalid date/time value
+        vevent = """BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=Europe/London:20120713T100000
+DTEND;TZID=Europe/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailt
+ o:jane.doe at example.org
+ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailt
+ o:user.external at example.com
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+"""
+        event = event_from_ical(vevent)
+        self.assertRaises(EventIntegrityError, event.to_message)
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index 75939d0..650879b 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -44,7 +44,7 @@ CALSCALE:GREGORIAN
 METHOD:REQUEST
 BEGIN:VEVENT
 UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
-DTSTAMP:20120713T1254140
+DTSTAMP:20120713T125414Z
 DTSTART;TZID=3DEurope/London:20120713T100000
 DTEND;TZID=3DEurope/London:20120713T110000
 SUMMARY:test


commit 0438fc64173d5e68822b7cf922ba807e1cabf95a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 10:21:49 2014 -0400

    Improve iCal import: support all event properties including alarms and attachments. We require full support if wallace directly copies invitations into user calendars

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 7b0c811..fcb3a17 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -31,6 +31,12 @@ class Event(object):
             "CANCELLED": kolabformat.StatusCancelled,
         }
 
+    classification_map = {
+            "PUBLIC": kolabformat.ClassPublic,
+            "PRIVATE": kolabformat.ClassPrivate,
+            "CONFIDENTIAL": kolabformat.ClassConfidential,
+        }
+
     def __init__(self, from_ical="", from_string=""):
         self._attendees = []
         self._categories = []
@@ -56,7 +62,7 @@ class Event(object):
         self.event.setAttendees(self._attendees)
 
     def add_category(self, category):
-        self._categories.append(category)
+        self._categories.append(str(category))
         self.event.setCategories(self._categories)
 
     def add_exception_date(self, _datetime):
@@ -166,12 +172,18 @@ class Event(object):
         self.event.setAttendees(self._attendees)
 
     def from_ical(self, ical):
-        self.event = kolabformat.Event()
         if 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)
 
+        # 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']:
+            self._xml_from_ical(ical)
+        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)):
@@ -188,13 +200,10 @@ class Event(object):
             if ical_event.has_key(attr):
                 self.set_from_ical(attr.lower(), ical_event[attr])
 
-        # HACK: use calendaring::EventCal::fromICal() to parse RRULEs
-        if ical_event.has_key('RRULE'):
-            from kolab.calendaring import EventCal
-            event_xml = EventCal()
-            event_xml.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
-            self.event.setRecurrenceRule(event_xml.recurrenceRule())
-            self.event.setExceptionDates(event_xml.exceptionDates())
+    def _xml_from_ical(self, ical):
+        from kolab.calendaring import EventCal
+        self.event = EventCal()
+        self.event.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
 
     def get_attendee_participant_status(self, attendee):
         return attendee.get_participant_status()
@@ -234,10 +243,10 @@ class Event(object):
         return self._attendees
 
     def get_categories(self):
-        return self.event.categories()
+        return [str(c) for c in self.event.categories()]
 
     def get_classification(self):
-        return self.classification()
+        return self.event.classification()
 
     def get_created(self):
         try:
@@ -273,6 +282,12 @@ class Event(object):
     def get_exception_dates(self):
         return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates())
 
+    def get_attachments(self):
+        return self.event.attachments()
+
+    def get_alarms(self):
+        return self.event.alarms()
+
     def get_ical_attendee(self):
         # TODO: Formatting, aye? See also the example snippet:
         #
@@ -393,6 +408,9 @@ class Event(object):
     def get_ical_sequence(self):
         return str(self.event.sequence()) if self.event.sequence() else None
 
+    def get_location(self):
+        return self.event.location()
+
     def get_lastmodified(self):
         try:
             _datetime = self.event.lastModified()
@@ -433,6 +451,9 @@ class Event(object):
     def get_sequence(self):
         return self.event.sequence()
 
+    def get_url(self):
+        return self.event.url()
+
     def get_transparency(self):
         return self.event.transparency()
 
@@ -449,8 +470,21 @@ class Event(object):
         attendee.set_participant_status(status)
         self.event.setAttendees(self._attendees)
 
+    def set_status(self, status):
+        if status in self.status_map.keys():
+            self.event.setStatus(self.status_map[status])
+        elif status in self.status_map.values():
+            self.event.setStatus(status)
+        else:
+            raise ValueError, _("Invalid status %r") % (status)
+
     def set_classification(self, classification):
-        self.event.setClassification(classification)
+        if classification in self.classification_map.keys():
+            self.event.setClassification(self.classification_map[classification])
+        elif classification in self.classification_map.values():
+            self.event.setClassification(status)
+        else:
+            raise ValueError, _("Invalid classification %r") % (classification)
 
     def set_created(self, _datetime=None):
         if _datetime == None:
@@ -459,7 +493,7 @@ class Event(object):
         self.event.setCreated(xmlutils.to_cdatetime(_datetime, False))
 
     def set_description(self, description):
-        self.event.setDescription(description)
+        self.event.setDescription(str(description))
 
     def set_dtstamp(self, _datetime):
         self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
@@ -486,26 +520,27 @@ class Event(object):
             self.add_exception_date(_datetime)
 
     def set_from_ical(self, attr, value):
+        ical_setter = 'set_ical_' + attr
+        default_setter = 'set_' + attr
+
         if attr == "dtend":
             self.set_ical_dtend(value.dt)
         elif attr == "dtstart":
             self.set_ical_dtstart(value.dt)
-        elif attr == "duration":
-            self.set_ical_duration(value)
-        elif attr == "status":
-            self.set_ical_status(value)
-        elif attr == "summary":
-            self.set_ical_summary(value)
-        elif attr == "priority":
-            self.set_ical_priority(value)
-        elif attr == "sequence":
-            self.set_ical_sequence(value)
-        elif attr == "attendee":
-            self.set_ical_attendee(value)
-        elif attr == "organizer":
-            self.set_ical_organizer(value)
-        elif attr == "uid":
-            self.set_ical_uid(value)
+        elif attr == "dtstamp":
+            self.set_ical_dtstamp(value.dt)
+        elif attr == "created":
+            self.set_created(value.dt)
+        elif attr == "lastmodified":
+            self.set_lastmodified(value.dt)
+        elif attr == "categories":
+            self.add_category(value)
+        elif attr == "class":
+            self.set_classification(value)
+        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):
@@ -556,6 +591,9 @@ class Event(object):
     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 value.dt:
             duration = kolabformat.Duration(value.dt.days, 0, 0, value.dt.seconds, False)
@@ -582,14 +620,6 @@ class Event(object):
     def set_ical_sequence(self, sequence):
         self.set_sequence(sequence)
 
-    def set_ical_status(self, status):
-        if status in self.status_map.keys():
-            self.event.setStatus(self.status_map[status])
-        elif status in self.status_map.values():
-            self.event.setStatus(status)
-        else:
-            raise ValueError, _("Invalid status %r") % (status)
-
     def set_ical_summary(self, summary):
         self.set_summary(str(summary))
 
@@ -614,7 +644,7 @@ class Event(object):
         self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
 
     def set_location(self, location):
-        self.event.setLocation(location)
+        self.event.setLocation(str(location))
 
     def set_organizer(self, email, name=None):
         contactreference = ContactReference(email)
@@ -629,6 +659,9 @@ class Event(object):
     def set_sequence(self, sequence):
         self.event.setSequence(int(sequence))
 
+    def set_url(self, url):
+        self.event.setUrl(str(url))
+
     def set_recurrence(self, recurrence):
         self.event.setRecurrenceRule(recurrence)
 
@@ -720,6 +753,8 @@ class Event(object):
 
         part.set_payload(str(self))
 
+        # TODO: extract attachment data to separate MIME parts
+
         part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"')
         part.replace_header('Content-Transfer-Encoding', '8bit')
 
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index a44c4ec..f9ef92e 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -118,25 +118,70 @@ class TestEventXML(unittest.TestCase):
     def test_018_load_from_ical(self):
         ical_str = """BEGIN:VCALENDAR
 VERSION:2.0
-PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+  2.1.3//EN
 CALSCALE:GREGORIAN
+METHOD:REQUEST
 BEGIN:VEVENT
+UID:7a35527d-f783-4b58-b404-b1389bd2fc57
+DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
+CREATED;VALUE=DATE-TIME:20140407T122245Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z
 DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000
 DURATION:PT1H30M0S
 RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
 EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000
 EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000
-UID:7a35527d-f783-4b58-b404-b1389bd2fc57
-ATTENDEE;CN="Doe, Jane";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED
- ;ROLE=REQ-PARTICIPANT;RSVP=FALSE:MAILTO:jane at doe.org
-ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION
- ;ROLE=OPT-PARTICIPANT;RSVP=FALSE:MAILTO:max at imum.com
+SUMMARY:Summary
+LOCATION:Location
+DESCRIPTION:Description\\n2 lines
+CATEGORIES:Personal
+TRANSP:OPAQUE
+PRIORITY:2
 SEQUENCE:2
+CLASS:PUBLIC
+ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP
+ E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager at example.org
+ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA
+ LSE:MAILTO:max at imum.com
+ORGANIZER;CN=Doe\, John:mailto:john.doe at example.org
+URL:http://somelink.com/foo
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn
+ g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i
+ ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF
+ xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX
+ V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS
+ bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U
+ eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV
+ m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg
+ lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d
+ ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q
+ QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3
+ ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo
+ ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf
+ vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr
+ tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/
+ Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg==
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh
+ pcyBpcyBhIHRleHQgZmlsZQo=
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER:-PT30M
+END:VALARM
 END:VEVENT
 END:VCALENDAR
 """
         ical = icalendar.Calendar.from_ical(ical_str)
         event = event_from_ical(ical.walk('VEVENT')[0].to_ical())
+
+        self.assertEqual(event.get_location(), "Location")
+        self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11")
+        self.assertEqual(event.get_description(), "Description\n2 lines")
+        self.assertEqual(event.get_url(), "http://somelink.com/foo")
+        self.assertEqual(event.get_transparency(), False)
+        self.assertEqual(event.get_categories(), ["Personal"])
+        self.assertEqual(event.get_priority(), '2')
+        self.assertEqual(event.get_classification(), kolabformat.ClassPublic)
         self.assertEqual(event.get_attendee_by_email("max at imum.com").get_cutype(), kolabformat.CutypeResource)
         self.assertEqual(event.get_sequence(), 2)
         self.assertTrue(event.is_recurring())
@@ -145,6 +190,8 @@ END:VCALENDAR
         self.assertEqual(str(event.get_end()), "2014-05-23 12:30:00+01:00")
         self.assertEqual(len(event.get_exception_dates()), 2)
         self.assertIsInstance(event.get_exception_dates()[0], datetime.datetime)
+        self.assertEqual(len(event.get_alarms()), 1)
+        self.assertEqual(len(event.get_attachments()), 2)
 
     def test_019_as_string_itip(self):
         self.event.set_summary("test")


commit cfc64210ee11c8e48e83a534059159e649d92906
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 08:14:53 2014 -0400

    Implement (basic) notification to organizer when processing iTip REPLY messages from attendees

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 56699ce..579158e 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -132,8 +132,17 @@ class Attendee(kolabformat.Attendee):
     def get_name(self):
         return self.contactreference.get_name()
 
-    def get_participant_status(self):
-        return self.partStat()
+    def get_displayname(self):
+        name = self.contactreference.get_name()
+        email = self.contactreference.get_email()
+        return "%s <%s>" % (name, email) if name is not None else email
+
+    def get_participant_status(self, translated=False):
+        partstat = self.partStat()
+        if translated:
+            partstat_name_map = dict([(v, k) for (k, v) in self.participant_status_map.iteritems()])
+            return partstat_name_map[partstat] if partstat_name_map.has_key(partstat) else 'UNKNOWN'
+        return partstat
 
     def get_role(self):
         return self.role()
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 10a377f..4fd61a7 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -10,6 +10,7 @@ import kolabformat
 from pykolab.imap import IMAP
 from wallace import module_resources
 
+from pykolab.translate import _
 from email import message_from_string
 from twisted.trial import unittest
 
@@ -129,6 +130,7 @@ Content-Transfer-Encoding: 8bit
 class TestWallaceInvitationpolicy(unittest.TestCase):
 
     john = None
+    itip_reply_subject = None
 
     @classmethod
     def setUp(self):
@@ -139,6 +141,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     @classmethod
     def setup_class(self, *args, **kw):
+        self.itip_reply_subject = _('"%(summary)s" has been %(status)s')
+
         from tests.functional.purge_users import purge_users
         purge_users()
 
@@ -147,9 +151,10 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'mail': 'john.doe at example.org',
             'sender': 'John Doe <john.doe at example.org>',
             'dn': 'uid=doe,ou=People,dc=example,dc=org',
+            'preferredlanguage': 'en_US',
             'mailbox': 'user/john.doe at example.org',
             'kolabtargetfolder': 'user/john.doe/Calendar at example.org',
-            'kolabinvitationpolicy': ['ACT_UPDATE', 'ACT_MANUAL']
+            'kolabinvitationpolicy': ['ACT_UPDATE_AND_NOTIFY','ACT_MANUAL']
         }
 
         self.jane = {
@@ -157,14 +162,27 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'mail': 'jane.manager at example.org',
             'sender': 'Jane Manager <jane.manager at example.org>',
             'dn': 'uid=manager,ou=People,dc=example,dc=org',
+            'preferredlanguage': 'en_US',
             'mailbox': 'user/jane.manager at example.org',
             'kolabtargetfolder': 'user/jane.manager/Calendar at example.org',
-            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT', 'ACT_UPDATE']
+            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','ACT_UPDATE']
+        }
+
+        self.jack = {
+            'displayname': 'Jack Tentative',
+            'mail': 'jack.tentative at example.org',
+            'sender': 'Jack Tentative <jack.tentative at example.org>',
+            'dn': 'uid=tentative,ou=People,dc=example,dc=org',
+            'preferredlanguage': 'en_US',
+            'mailbox': 'user/jack.tentative at example.org',
+            'kolabtargetfolder': 'user/jack.tentative/Calendar at example.org',
+            'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
         }
 
         from tests.functional.user_add import user_add
-        user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'])
-        user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'])
+        user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage'])
+        user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage'])
+        user_add("Jack", "Tentative", kolabinvitationpolicy=self.jack['kolabinvitationpolicy'], preferredlanguage=self.jack['preferredlanguage'])
 
         time.sleep(1)
         from tests.functional.synchronize import synchronize_once
@@ -223,7 +241,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def send_itip_reply(self, uid, mailto, attendee_email, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'):
+    def send_itip_reply(self, uid, attendee_email, mailto, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'):
         if start is None:
             start = datetime.datetime.now()
 
@@ -256,13 +274,13 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendee=None):
+    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None):
         if start is None:
             start = datetime.datetime.now(pytz.timezone("Europe/Berlin"))
         if user is None:
             user = self.john
-        if attendee is None:
-            attendee = self.jane
+        if attendees is None:
+            attendees = [self.jane]
 
         end = start + datetime.timedelta(hours=4)
 
@@ -270,7 +288,10 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         event.set_start(start)
         event.set_end(end)
         event.set_organizer(user['mail'], user['displayname'])
-        event.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+
+        for attendee in attendees:
+            event.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+
         event.set_summary(summary)
         event.set_sequence(sequence)
 
@@ -372,11 +393,11 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         imap.disconnect()
 
 
-    def test_001_invite_user(self):
+    def test_001_invite_accept_udate(self):
         start = datetime.datetime(2014,8,13, 10,0,0)
         uid = self.send_itip_invitation(self.jane['mail'], start)
 
-        response = self.check_message_received('"test" has been ACCEPTED', self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -394,10 +415,10 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
 
     # @depends on test_001_invite_user
-    def test_002_invite_conflict(self):
+    def test_002_invite_conflict_reject(self):
         uid = self.send_itip_invitation(self.jane['mail'], datetime.datetime(2014,8,13, 11,0,0), summary="test2")
 
-        response = self.check_message_received('"test2" has been DECLINED', self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':_('DECLINED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -405,11 +426,40 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(event.get_summary(), "test2")
 
 
-    def test_003_invite_rescheduling(self):
+    def test_003_invite_accept_tentative(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,24, 8,0,0))
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+
+    def test_004_copy_to_calendar(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 8,0,0))
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('TENTATIVE') }, self.jack['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        # send conflicting request to jack
+        uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 10,0,0), summary="test2")
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':_('DECLINED') }, self.jack['mail'])
+        self.assertEqual(response, None, "No reply expected")
+
+        event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test2")
+        self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+
+
+    def test_005_invite_rescheduling_accept(self):
+        self.purge_mailbox(self.john['mailbox'])
+
         start = datetime.datetime(2014,8,14, 9,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.send_itip_invitation(self.jane['mail'], start)
 
-        response = self.check_message_received('"test" has been ACCEPTED', self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -422,7 +472,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         new_start = datetime.datetime(2014,8,15, 15,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         self.send_itip_update(self.jane['mail'], uid, new_start, summary="test", sequence=1)
 
-        response = self.check_message_received('"test" has been ACCEPTED', self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':_('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
@@ -431,7 +481,13 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(event.get_sequence(), 1)
 
 
-    def test_004_invitation_reply(self):
+    def test_005_invite_rescheduling_reject(self):
+        pass
+
+
+    def test_006_invitation_reply(self):
+        self.purge_mailbox(self.john['mailbox'])
+
         start = datetime.datetime(2014,8,18, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.create_calendar_event(start, user=self.john)
 
@@ -439,7 +495,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send a reply from jane to john
-        self.send_itip_reply(uid, self.john['mail'], self.jane['mail'], start=start)
+        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
 
         # check for the updated event in john's calendar
         time.sleep(10)
@@ -450,10 +506,13 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIsInstance(attendee, pykolab.xml.Attendee)
         self.assertEqual(attendee.get_participant_status(), kolabformat.PartAccepted)
 
-    def test_005_invitation_cancel(self):
+
+    def test_007_invitation_cancel(self):
+        self.purge_mailbox(self.john['mailbox'])
+
         uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled")
 
-        response = self.check_message_received('"cancelled" has been ACCEPTED', self.jane['mail'])
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':_('ACCEPTED') }, self.jane['mail'])
         self.assertIsInstance(response, email.message.Message)
 
         self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
@@ -465,4 +524,33 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(event.get_status(), 'CANCELLED')
         self.assertTrue(event.get_transparency())
 
-        
\ No newline at end of file
+
+    def test_008_inivtation_reply_notify(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack])
+
+        # send a reply from jane to john
+        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
+
+        # check for notification message
+        # TODO: this notification should be suppressed until jack has replied, too
+        notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+        self.assertIsInstance(notification, email.message.Message)
+
+        notification_text = str(notification.get_payload());
+        self.assertIn(self.jane['mail'], notification_text)
+        self.assertIn(_("PENDING"), notification_text)
+
+        self.purge_mailbox(self.john['mailbox'])
+
+        # send a reply from jack to john
+        self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE')
+
+        notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+        self.assertIsInstance(notification, email.message.Message)
+
+        notification_text = str(notification.get_payload());
+        self.assertIn(self.jack['mail'], notification_text)
+        self.assertNotIn(_("PENDING"), notification_text)
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index d4ed7d5..a141251 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -34,6 +34,7 @@ import modules
 import pykolab
 import kolabformat
 
+from pykolab import utils
 from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
@@ -45,21 +46,22 @@ from pykolab.itip import send_reply
 from pykolab.translate import _
 
 # define some contstants used in the code below
-MOD_IF_AVAILABLE   = 32
-MOD_IF_CONFLICT    = 64
-MOD_TENTATIVE      = 128
-MOD_NOTIFY         = 256
+COND_IF_AVAILABLE  = 32
+COND_IF_CONFLICT   = 64
+COND_TENTATIVE     = 128
+COND_NOTIFY        = 256
 ACT_MANUAL         = 1
 ACT_ACCEPT         = 2
 ACT_DELEGATE       = 4
 ACT_REJECT         = 8
 ACT_UPDATE         = 16
-ACT_TENTATIVE                = ACT_ACCEPT + MOD_TENTATIVE
-ACT_ACCEPT_IF_NO_CONFLICT    = ACT_ACCEPT + MOD_IF_AVAILABLE
-ACT_TENTATIVE_IF_NO_CONFLICT = ACT_ACCEPT + MOD_TENTATIVE + MOD_IF_AVAILABLE
-ACT_DELEGATE_IF_CONFLICT     = ACT_DELEGATE + MOD_IF_CONFLICT
-ACT_REJECT_IF_CONFLICT       = ACT_REJECT + MOD_IF_CONFLICT
-ACT_UPDATE_AND_NOTIFY        = ACT_UPDATE + MOD_NOTIFY
+ACT_TENTATIVE                = ACT_ACCEPT + COND_TENTATIVE
+ACT_ACCEPT_IF_NO_CONFLICT    = ACT_ACCEPT + COND_IF_AVAILABLE
+ACT_TENTATIVE_IF_NO_CONFLICT = ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE
+ACT_DELEGATE_IF_CONFLICT     = ACT_DELEGATE + COND_IF_CONFLICT
+ACT_REJECT_IF_CONFLICT       = ACT_REJECT + COND_IF_CONFLICT
+ACT_UPDATE_AND_NOTIFY        = ACT_UPDATE + COND_NOTIFY
+ACT_SAVE_TO_CALENDAR         = 512
 
 FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
 
@@ -77,7 +79,8 @@ policy_name_map = {
     'ACT_REJECT':                   ACT_REJECT,
     'ACT_REJECT_IF_CONFLICT':       ACT_REJECT_IF_CONFLICT,
     'ACT_UPDATE':                   ACT_UPDATE,
-    'ACT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY
+    'ACT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY,
+    'ACT_SAVE_TO_CALENDAR':         ACT_SAVE_TO_CALENDAR
 }
 
 policy_value_map = dict([(v, k) for (k, v) in policy_name_map.iteritems()])
@@ -241,7 +244,11 @@ def execute(*args, **kw):
         return filepath
 
     receiving_user = auth.get_entry_attributes(None, recipient_user_dn, ['*'])
-    log.debug(_("Receiving user: %r") % (receiving_user), level=9)
+    log.debug(_("Receiving user: %r") % (receiving_user), level=8)
+
+    # change gettext language to the preferredlanguage setting of the receiving user
+    if receiving_user.has_key('preferredlanguage'):
+        pykolab.translate.setUserLanguage(receiving_user['preferredlanguage'])
 
     # find user's kolabInvitationPolicy settings and the matching policy values
     sender_domain = str(sender_email).split('@')[-1]
@@ -318,9 +325,9 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
 
     # if scheduling: check availability
     if scheduling_required:
-        if policy & (MOD_IF_AVAILABLE | MOD_IF_CONFLICT):
+        if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
             condition_fulfilled = check_availability(itip_event, receiving_user)
-        if policy & MOD_IF_CONFLICT:
+        if policy & COND_IF_CONFLICT:
             condition_fulfilled = not condition_fulfilled
 
         log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
@@ -329,15 +336,15 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
     if rsvp or scheduling_required:
         respond_with = None
         if policy & ACT_ACCEPT and condition_fulfilled:
-            respond_with = 'TENTATIVE' if policy & MOD_TENTATIVE else 'ACCEPTED'
+            respond_with = 'TENTATIVE' if policy & COND_TENTATIVE else 'ACCEPTED'
 
         elif policy & ACT_REJECT and condition_fulfilled:
             respond_with = 'DECLINED'
             # TODO: only save declined invitation when a certain config option is set?
 
         elif policy & ACT_DELEGATE and condition_fulfilled:
-            # TODO: save and delegate (but to whom?)
-            pass
+            # TODO: delegate (but to whom?)
+            return None
 
         # send iTip reply
         if respond_with is not None:
@@ -349,9 +356,9 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             send_reply(recipient_email, itip_event, invitation_response_text(),
                 subject=_('"%(summary)s" has been %(status)s'))
 
-        # elif partstat == kolabformat.PartNeedsAction and conf.get('wallace','invitationpolicy_always_copy_to_calendar'):
-            # TODO: copy the invitation into the user's calendar with unchanged PARTSTAT
-            # TODO: or use ACT_POSTPONE for this?
+        elif policy & ACT_SAVE_TO_CALENDAR:
+            # copy the invitation into the user's calendar with unchanged PARTSTAT
+            save_event = True
 
         else:
             # policy doesn't match, pass on to next one
@@ -413,7 +420,9 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
 
             # update the organizer's copy of the event
             if update_event(existing, receiving_user):
-                # TODO: send (consolidated) notification to organizer if policy & ACT_UPDATE_AND_NOTIFY:
+                if policy & COND_NOTIFY:
+                    send_reply_notification(existing, receiving_user)
+
                 # TODO: update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
                 return MESSAGE_PROCESSED
 
@@ -742,6 +751,65 @@ def delete_event(existing):
     imap.imap.m.expunge()
 
 
+def send_reply_notification(event, receiving_user):
+    """
+        Send a (consolidated) notification about the current participant status to organizer
+    """
+    import smtplib
+    from email.MIMEText import MIMEText
+    from email.Utils import formatdate
+
+    log.debug(_("Compose participation status summary for event %r to user %r") % (
+        event.uid, receiving_user['mail']
+    ), level=8)
+
+    partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] }
+    for attendee in event.get_attendees():
+        parstat = attendee.get_participant_status(True)
+        if partstats.has_key(parstat):
+            partstats[parstat].append(attendee.get_displayname())
+        else:
+            partstats['PENDING'].append(attendee.get_displayname())
+
+    # TODO: for every attendee, look-up its kolabinvitationpolicy and skip notification
+    # until we got replies from all automatically responding attendees
+
+    roundup = ''
+    for status,attendees in partstats.iteritems():
+        if len(attendees) > 0:
+            roundup += "\n" + _(status) + ":\n" + "\n".join(attendees) + "\n"
+
+    message_text = """
+        The event '%(summary)s' at %(start)s has been updated in your calendar.
+        %(roundup)s
+    """ % {
+        'summary': event.get_summary(),
+        'start': event.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+        'roundup': roundup
+    }
+
+    # compose mime message
+    msg = MIMEText(utils.stripped_message(message_text))
+
+    msg['To'] = receiving_user['mail']
+    msg['Date'] = formatdate(localtime=True)
+    msg['Subject'] = _('"%s" has been updated') % (event.get_summary())
+
+    organizer = event.get_organizer()
+    orgemail = organizer.email()
+    orgname = organizer.name()
+
+    msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
+
+    smtp = smtplib.SMTP("localhost", 10027)
+
+    if conf.debuglevel > 8:
+        smtp.set_debuglevel(True)
+
+    smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    smtp.quit()
+
+
 def invitation_response_text():
     return _("""
         %(name)s has %(status)s your invitation for %(summary)s.


commit cf500d4b24cf865d77bf09e2bf149da2cf09421a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 07:21:27 2014 -0400

    Add function to change user language; add en.po for English localization

diff --git a/po/de.po b/po/de.po
index ff7abe2..b49222d 100644
--- a/po/de.po
+++ b/po/de.po
@@ -721,7 +721,7 @@ msgstr ""
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54
 #: ../pykolab/cli/cmd_set_mailbox_metadata.py:54
 msgid "Folder name"
-msgstr ""
+msgstr "Ordnername"
 
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:60
 #: ../pykolab/cli/cmd_list_mailbox_acls.py:52
diff --git a/po/de_DE.po b/po/de_DE.po
index 03f16f2..e849ee9 100644
--- a/po/de_DE.po
+++ b/po/de_DE.po
@@ -686,7 +686,7 @@ msgstr ""
 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54
 #: ../pykolab/cli/cmd_set_mailbox_metadata.py:65
 msgid "Folder name"
-msgstr ""
+msgstr "Ordnername"
 
 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:60
 #: ../pykolab/cli/cmd_list_mailbox_acls.py:54
diff --git a/po/en.po b/po/en.po
new file mode 100644
index 0000000..da6a905
--- /dev/null
+++ b/po/en.po
@@ -0,0 +1,15 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# 
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Groupware Solution\n"
+"Report-Msgid-Bugs-To: https://isues.kolab.org/\n"
+"POT-Creation-Date: 2014-07-17 10:22+0100\n"
+"PO-Revision-Date: 2014-07-14 11:13+0000\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
diff --git a/pykolab/translate.py b/pykolab/translate.py
index bee8fc2..85f4516 100644
--- a/pykolab/translate.py
+++ b/pykolab/translate.py
@@ -28,6 +28,8 @@ import os
 N_ = lambda x: x
 _ = lambda x: gettext.ldgettext(domain, x)
 
+#gettext.bindtextdomain(domain, '/usr/local/share/locale')
+
 def getDefaultLangs():
     languages = []
     for envar in ('LANGUAGE', 'LC_ALL', 'LC_MESSAGES', 'LANG'):
@@ -45,3 +47,17 @@ def getDefaultLangs():
             if nelang not in nelangs:
                 nelangs.append(nelang)
     return nelangs
+
+def setUserLanguage(lang):
+    langs = []
+    for l in gettext._expand_lang(lang):
+        if l not in langs:
+            langs.append(l)
+
+    try:
+        translation = gettext.translation(domain, languages=langs)
+        translation.install()
+    except:
+        return False
+
+    return True
diff --git a/tests/unit/test-015-translate.py b/tests/unit/test-015-translate.py
new file mode 100644
index 0000000..8ca9463
--- /dev/null
+++ b/tests/unit/test-015-translate.py
@@ -0,0 +1,25 @@
+import unittest
+import gettext
+from pykolab import translate
+
+class TestTranslate(unittest.TestCase):
+
+    def setUp(self):
+        translate.setUserLanguage('en')
+
+    def test_001_default_langs(self):
+        self.assertTrue(len(translate.getDefaultLangs()) > 0)
+
+    def test_002_translate(self):
+        from pykolab.translate import _
+        self.assertEqual(_("Folder name"), "Folder name")
+
+    def test_003_set_lang(self):
+        from pykolab.translate import _
+        self.assertFalse(translate.setUserLanguage('foo_bar'))
+        self.assertEqual(_("Folder name"), "Folder name")
+        self.assertTrue(translate.setUserLanguage('de_DE'))
+        self.assertEqual(_("Folder name"), "Ordnername")
+
+if __name__ == '__main__':
+    unittest.main()


commit 2dd455c056baad1acc059273645ddf696027b2c8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 03:18:31 2014 -0400

    Fix typo

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 42e08dd..f32cad4 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -100,7 +100,7 @@ def objects_from_message(message, objname, methods=None):
 
                     itip['attendees'] = c['attendee']
 
-                    if itip.has_key('attendee') and not isinstance(itip['attendees'], list):
+                    if itip.has_key('attendees') and not isinstance(itip['attendees'], list):
                         itip['attendees'] = [c['attendee']]
 
                     if c.has_key('resources'):


commit 0703a82c800de826245e4bd6da6061a567c2422c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 01:59:03 2014 -0400

    Correctly return list of tuples from patched auth.search_entry_by_attribute() method

diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py
index ccec4b1..9c42317 100644
--- a/tests/unit/test-011-wallace_resources.py
+++ b/tests/unit/test-011-wallace_resources.py
@@ -127,7 +127,7 @@ class TestWallaceResources(unittest.TestCase):
     def _mock_search_entry_by_attribute(self, attr, value, **kw):
         results = []
         if value == "cn=Room 101,ou=Resources,dc=example,dc=org":
-            results.append({ 'dn': 'cn=Rooms,ou=Resources,dc=example,dc=org', attr: value, 'owner': 'uid=doe,ou=People,dc=example,dc=org' })
+            results.append(('cn=Rooms,ou=Resources,dc=example,dc=org', { attr: value, 'owner': 'uid=doe,ou=People,dc=example,dc=org' }))
         return results
 
     def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 7c23995..f398120 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -809,7 +809,7 @@ def get_resource_owner(resource):
         if not isinstance(collections, list):
             collections = [ collections ]
 
-        for collection in collections:
+        for dn,collection in collections:
             if collection.has_key('owner') and isinstance(collection['owner'], list):
                 owners += collection['owner']
             elif collection.has_key('owner'):


commit d80f5c0fbb69f7b976275563e8b9b9521e9ca55e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 01:49:12 2014 -0400

    Move unit tests for pykolab.itip to a separate file; fix failing wallace module test

diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py
new file mode 100644
index 0000000..abbaa92
--- /dev/null
+++ b/tests/unit/test-011-itip.py
@@ -0,0 +1,400 @@
+import pykolab
+import datetime
+import pytz
+import kolabformat
+
+from pykolab import itip
+from pykolab.xml import Event
+
+from icalendar import Calendar
+from email import message
+from email import message_from_string
+from wallace import module_resources
+from twisted.trial import unittest
+
+# define some iTip MIME messages
+
+itip_multipart = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe at example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7 at example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: resource-collection-car at example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Disposition: attachment;
+ filename=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:resource-collection-car at example.org
+ATTENDEE;ROLE=3DOPT-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:anoth=
+er-resource at example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+itip_non_multipart = """Return-Path: <john.doe at example.org>
+Sender: john.doe at example.org
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+To: resource-collection-car at example.org
+From: john.doe at example.org
+Date: Mon, 24 Feb 2014 11:27:28 +0100
+Message-ID: <1a3aa8995e83dd24cf9247e538ac913a at example.org>
+Subject: test
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSVP=3DTRUE:mailt=
+o:resource-collection-car at example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_google_multipart = """MIME-Version: 1.0
+Message-ID: <001a11c2ad84243e0604f3246bae at google.com>
+Date: Mon, 24 Feb 2014 10:27:28 +0000
+Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?=
+	=?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?=
+From: "john.doe" <john.doe at gmail.com>
+To: <john.sample at example.org>
+Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad
+
+--001a11c2ad84243df004f3246bad
+Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab
+
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
+
+<some text content here>
+
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+<div style=3D""><!-- some HTML message content here --></div>
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20140224T110000Z
+DTEND:20140224T120000Z
+DTSTAMP:20140224T102728Z
+ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000 at group.calendar.google.com
+UID:0BE2F640-5814-47C9-ABAE-E7E959204E76
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000 at group.calendar.google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample at example.org
+CREATED:20140224T102728Z
+DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog
+ le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M
+ WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N
+ TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz
+ NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en
+ .
+LAST-MODIFIED:20140224T102728Z
+LOCATION:
+SEQUENCE:5
+STATUS:CONFIRMED
+SUMMARY:iTip from Apple
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--001a11c2ad84243dec04f3246bab--
+--001a11c2ad84243df004f3246bad
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
+LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
+VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0
+VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq
+bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF
+MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ
+RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7
+WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh
+bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB
+UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT
+PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
+O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
+Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls
+LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp
+cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j
+b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3
+M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO
+bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz
+WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN
+CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6
+PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN
+CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy
+b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg==
+--001a11c2ad84243df004f3246bad--
+"""
+
+itip_application_ics = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe at example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c101622 at example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: resource-collection-car at example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=UTF-8; format=flowed
+
+<some text here>
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: application/ics; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:resource-collection-car at example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+itip_recurring = """Return-Path: <john.doe at example.org>
+Sender: john.doe at example.org
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+From: john.doe at example.org
+Date: Mon, 24 Feb 2014 11:27:28 +0100
+Message-ID: <1a3aa8995e83dd24cf9247e538ac913a at example.org>
+Subject: Recurring
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:dbdb8baeedacae836230e3436fd-5e83dd24cf92
+DTSTAMP:20140213T1254140
+DTSTART;TZID=Europe/London:20120709T100000
+DTEND;TZID=Europe/London:20120709T120000
+RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5
+SUMMARY:Recurring
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:jane at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_empty = """MIME-Version: 1.0
+Date: Fri, 17 Jan 2014 13:51:50 +0100
+From: <john.doe at example.org>
+User-Agent: Roundcube Webmail/0.9.5
+To: john.sample at example.org
+Subject: "test" has been sent
+Message-ID: <52D92766.5040508 at somedomain.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Message plain text goes here...
+"""
+
+conf = pykolab.getConf()
+
+if not hasattr(conf, 'defaults'):
+    conf.finalize_conf()
+
+class TestITip(unittest.TestCase):
+
+    def setUp(self):
+        # intercept calls to smtplib.SMTP.sendmail()
+        import smtplib
+        self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init)
+        self.patch(smtplib.SMTP, "quit", self._mock_nop)
+        self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail)
+
+        self.smtplog = [];
+
+    def _mock_nop(self, domain=None):
+        pass
+
+    def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
+        pass
+
+    def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None):
+        self.smtplog.append((from_addr, to_addr, message))
+
+
+    def test_001_itip_events_from_message(self):
+        itips1 = itip.events_from_message(message_from_string(itip_multipart))
+        self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar")
+        self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property")
+
+        itips2 = itip.events_from_message(message_from_string(itip_non_multipart))
+        self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages")
+
+        itips3 = itip.events_from_message(message_from_string(itip_application_ics))
+        self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment")
+
+        itips4 = itip.events_from_message(message_from_string(itip_google_multipart))
+        self.assertEqual(len(itips4), 1, "Multipart iTip message from Google")
+
+        itips5 = itip.events_from_message(message_from_string(itip_empty))
+        self.assertEqual(len(itips5), 0, "Simple plain text message")
+
+        # invalid itip blocks
+        self.assertRaises(Exception, itip.events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", "")))
+
+        itips6 = itip.events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;")))
+        self.assertEqual(len(itips6), 0, "Event with not DTSTART")
+
+        itips7 = itip.events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH")))
+        self.assertEqual(len(itips7), 0, "Invalid METHOD")
+
+
+    def test_002_check_date_conflict(self):
+        astart = datetime.datetime(2014,7,13, 10,0,0)
+        aend   = astart + datetime.timedelta(hours=2)
+
+        bstart = datetime.datetime(2014,7,13, 10,0,0)
+        bend   = astart + datetime.timedelta(hours=1)
+        self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+        bstart = datetime.datetime(2014,7,13, 11,0,0)
+        bend   = astart + datetime.timedelta(minutes=30)
+        self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+        bend   = astart + datetime.timedelta(hours=2)
+        self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+        bstart = datetime.datetime(2014,7,13, 12,0,0)
+        bend   = astart + datetime.timedelta(hours=1)
+        self.assertFalse(itip.check_date_conflict(astart, aend, bstart, bend))
+
+        bstart = datetime.datetime(2014,6,13, 10,0,0)
+        bend   = datetime.datetime(2014,6,14, 12,0,0)
+        self.assertFalse(itip.check_date_conflict(astart, aend, bstart, bend))
+
+        bstart = datetime.datetime(2014,7,10, 12,0,0)
+        bend   = datetime.datetime(2014,7,14, 14,0,0)
+        self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+
+    def test_002_check_event_conflict(self):
+        itip_event = itip.events_from_message(message_from_string(itip_non_multipart))[0]
+
+        event = Event()
+        event.set_start(datetime.datetime(2012,7,13, 9,30,0, tzinfo=itip_event['start'].tzinfo))
+        event.set_end(datetime.datetime(2012,7,13, 10,30,0, tzinfo=itip_event['start'].tzinfo))
+
+        self.assertTrue(itip.check_event_conflict(event, itip_event), "Conflicting dates")
+
+        event.set_uid(itip_event['uid'])
+        self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict for same UID")
+
+        event2 = Event()
+        event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central")))
+        event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central")))
+
+        self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict with timezone shift")
+
+        rrule = kolabformat.RecurrenceRule()
+        rrule.setFrequency(kolabformat.RecurrenceRule.Weekly)
+        rrule.setCount(10)
+
+        event3 = Event()
+        event3.set_recurrence(rrule);
+        event3.set_start(datetime.datetime(2012,6,29, 9,30,0, tzinfo=pytz.utc))
+        event3.set_end(datetime.datetime(2012,6,29, 10,30,0, tzinfo=pytz.utc))
+
+        self.assertTrue(itip.check_event_conflict(event3, itip_event), "Conflict in (3rd) recurring event instance")
+
+        itip_event = itip.events_from_message(message_from_string(itip_recurring))[0]
+        self.assertTrue(itip.check_event_conflict(event3, itip_event), "Conflict in two recurring events")
+
+        event4 = Event()
+        event4.set_recurrence(rrule);
+        event4.set_start(datetime.datetime(2012,7,1, 9,30,0, tzinfo=pytz.utc))
+        event4.set_end(datetime.datetime(2012,7,1, 10,30,0, tzinfo=pytz.utc))
+        self.assertFalse(itip.check_event_conflict(event4, itip_event), "No conflict in two recurring events")
+
+
+    def test_003_send_reply(self):
+        itip_events = itip.events_from_message(message_from_string(itip_non_multipart))
+        itip.send_reply("resource-collection-car at example.org", itip_events, "SUMMARY=%(summary)s; STATUS=%(status)s; NAME=%(name)s;")
+
+        self.assertEqual(len(self.smtplog), 1)
+        self.assertEqual(self.smtplog[0][0], 'resource-collection-car at example.org', "From attendee")
+        self.assertEqual(self.smtplog[0][1], 'john.doe at example.org', "To organizer")
+
+        message = message_from_string(self.smtplog[0][2])
+        self.assertEqual(message.get('Subject'), 'Invitation for test was ACCEPTED')
+
+        text = str(message.get_payload(0));
+        self.assertIn('SUMMARY=test', text)
+        self.assertIn('STATUS=ACCEPTED', text)
diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py
index bb586f8..ccec4b1 100644
--- a/tests/unit/test-011-wallace_resources.py
+++ b/tests/unit/test-011-wallace_resources.py
@@ -2,6 +2,7 @@ import pykolab
 import logging
 import datetime
 
+from pykolab import itip
 from icalendar import Calendar
 from email import message
 from email import message_from_string
@@ -87,152 +88,6 @@ END:VEVENT
 END:VCALENDAR
 """
 
-itip_google_multipart = """MIME-Version: 1.0
-Message-ID: <001a11c2ad84243e0604f3246bae at google.com>
-Date: Mon, 24 Feb 2014 10:27:28 +0000
-Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?=
-	=?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?=
-From: "john.doe" <john.doe at gmail.com>
-To: <john.sample at example.org>
-Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad
-
---001a11c2ad84243df004f3246bad
-Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab
-
---001a11c2ad84243dec04f3246bab
-Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
-
-<some text content here>
-
---001a11c2ad84243dec04f3246bab
-Content-Type: text/html; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-
-<div style=3D""><!-- some HTML message content here --></div>
---001a11c2ad84243dec04f3246bab
-Content-Type: text/calendar; charset=UTF-8; method=REQUEST
-Content-Transfer-Encoding: 7bit
-
-BEGIN:VCALENDAR
-PRODID:-//Google Inc//Google Calendar 70.9054//EN
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-BEGIN:VEVENT
-DTSTART:20140224T110000Z
-DTEND:20140224T120000Z
-DTSTAMP:20140224T102728Z
-ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000 at group.calendar.google.com
-UID:0BE2F640-5814-47C9-ABAE-E7E959204E76
-ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
- ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000 at group.calendar.google.com
-ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
- TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample at example.org
-CREATED:20140224T102728Z
-DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog
- le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M
- WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N
- TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz
- NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en
- .
-LAST-MODIFIED:20140224T102728Z
-LOCATION:
-SEQUENCE:5
-STATUS:CONFIRMED
-SUMMARY:iTip from Apple
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-
---001a11c2ad84243dec04f3246bab--
---001a11c2ad84243df004f3246bad
-Content-Type: application/ics; name="invite.ics"
-Content-Disposition: attachment; filename="invite.ics"
-Content-Transfer-Encoding: base64
-
-QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
-LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
-VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0
-VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq
-bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF
-MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ
-RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7
-WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh
-bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB
-UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT
-PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
-O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
-Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls
-LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp
-cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j
-b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3
-M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO
-bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz
-WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN
-CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6
-PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN
-CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy
-b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg==
---001a11c2ad84243df004f3246bad--
-"""
-
-itip_application_ics = """MIME-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="=_c8894dbdb8baeedacae836230e3436fd"
-From: "Doe, John" <john.doe at example.org>
-Date: Fri, 13 Jul 2012 13:54:14 +0100
-Message-ID: <240fe7ae7e139129e9eb95213c101622 at example.org>
-User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
-To: resource-collection-car at example.org
-Subject: "test" has been updated
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain; charset=UTF-8; format=flowed
-
-<some text here>
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Type: application/ics; charset=UTF-8; method=REQUEST;
- name=event.ics
-Content-Transfer-Encoding: quoted-printable
-
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-BEGIN:VEVENT
-UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
-DTSTAMP:20120713T1254140
-DTSTART;TZID=3DEurope/London:20120713T100000
-DTEND;TZID=3DEurope/London:20120713T110000
-SUMMARY:test
-DESCRIPTION:test
-ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
-ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
-o:resource-collection-car at example.org
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-
---=_c8894dbdb8baeedacae836230e3436fd--
-"""
-
-itip_empty = """MIME-Version: 1.0
-Date: Fri, 17 Jan 2014 13:51:50 +0100
-From: <john.doe at example.org>
-User-Agent: Roundcube Webmail/0.9.5
-To: john.sample at example.org
-Subject: "test" has been sent
-Message-ID: <52D92766.5040508 at somedomain.com>
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 7bit
-
-Message plain text goes here...
-"""
-
-
 conf = pykolab.getConf()
 
 if not hasattr(conf, 'defaults'):
@@ -301,32 +156,6 @@ class TestWallaceResources(unittest.TestCase):
 
         return None
 
-    def test_001_itip_events_from_message(self):
-        itips1 = pykolab.itip.events_from_message(message_from_string(itip_multipart))
-        self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar")
-        self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property")
-
-        itips2 = pykolab.itip.events_from_message(message_from_string(itip_non_multipart))
-        self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages")
-
-        itips3 = pykolab.itip.events_from_message(message_from_string(itip_application_ics))
-        self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment")
-
-        itips4 = pykolab.itip.events_from_message(message_from_string(itip_google_multipart))
-        self.assertEqual(len(itips4), 1, "Multipart iTip message from Google")
-
-        itips5 = pykolab.itip.events_from_message(message_from_string(itip_empty))
-        self.assertEqual(len(itips5), 0, "Simple plain text message")
-
-        # invalid itip blocks
-        self.assertRaises(Exception, pykolab.itip.events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", "")))
-
-        itips6 = pykolab.itip.events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;")))
-        self.assertEqual(len(itips6), 0, "Event with not DTSTART")
-
-        itips7 = pykolab.itip.events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH")))
-        self.assertEqual(len(itips7), 0, "Invalid METHOD")
-
 
     def test_002_resource_record_from_email_address(self):
         res = module_resources.resource_record_from_email_address("doe at example.org")
@@ -337,7 +166,7 @@ class TestWallaceResources(unittest.TestCase):
 
     def test_003_resource_records_from_itip_events(self):
         message = message_from_string(itip_multipart)
-        itips = pykolab.itip.events_from_message(message)
+        itips = itip.events_from_message(message)
 
         res = module_resources.resource_records_from_itip_events(itips)
         self.assertEqual(len(res), 2, "Return all attendee resources");
@@ -365,7 +194,7 @@ class TestWallaceResources(unittest.TestCase):
 
 
     def test_005_send_response_accept(self):
-        itip_event = pykolab.itip.events_from_message(message_from_string(itip_non_multipart))
+        itip_event = itip.events_from_message(message_from_string(itip_non_multipart))
         module_resources.send_response("resource-collection-car at example.org", itip_event)
 
         self.assertEqual(len(self.smtplog), 1);
@@ -384,7 +213,7 @@ class TestWallaceResources(unittest.TestCase):
 
     def test_006_send_response_delegate(self):
         # delegate resource-collection-car at example.org => resource-car-audi-a4 at example.org
-        itip_event = pykolab.itip.events_from_message(message_from_string(itip_non_multipart))[0]
+        itip_event = itip.events_from_message(message_from_string(itip_non_multipart))[0]
         itip_event['xml'].delegate('resource-collection-car at example.org', 'resource-car-audi-a4 at example.org')
         itip_event['xml'].set_attendee_participant_status(itip_event['xml'].get_attendee('resource-car-audi-a4 at example.org'), "ACCEPTED")
 
@@ -408,30 +237,3 @@ class TestWallaceResources(unittest.TestCase):
         self.assertEqual(ical2['attendee'].params['PARTSTAT'], "DELEGATED")
 
 
-    def test_007_check_date_conflict(self):
-        astart = datetime.datetime(2014,7,13, 10,0,0)
-        aend   = astart + datetime.timedelta(hours=2)
-
-        bstart = datetime.datetime(2014,7,13, 10,0,0)
-        bend   = astart + datetime.timedelta(hours=1)
-        self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
-        bstart = datetime.datetime(2014,7,13, 11,0,0)
-        bend   = astart + datetime.timedelta(minutes=30)
-        self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
-        bend   = astart + datetime.timedelta(hours=2)
-        self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
-        bstart = datetime.datetime(2014,7,13, 12,0,0)
-        bend   = astart + datetime.timedelta(hours=1)
-        self.assertFalse(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
-        bstart = datetime.datetime(2014,6,13, 10,0,0)
-        bend   = datetime.datetime(2014,6,14, 12,0,0)
-        self.assertFalse(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
-        bstart = datetime.datetime(2014,7,10, 12,0,0)
-        bend   = datetime.datetime(2014,7,14, 14,0,0)
-        self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f398120..7c23995 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -809,7 +809,7 @@ def get_resource_owner(resource):
         if not isinstance(collections, list):
             collections = [ collections ]
 
-        for dn,collection in collections:
+        for collection in collections:
             if collection.has_key('owner') and isinstance(collection['owner'], list):
                 owners += collection['owner']
             elif collection.has_key('owner'):


commit 951c796336c854474697754110b903c4ce90ccf5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon Jul 7 01:34:10 2014 -0400

    Set uid property, too

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index e438343..7b0c811 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -665,6 +665,7 @@ class Event(object):
         self.event.setSummary(summary)
 
     def set_uid(self, uid):
+        self.uid = uid
         self.event.setUid(str(uid))
 
     def set_transparency(self, transp):


commit 166d4f4d0649a3a5ab2d23b26d271f0579602720
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 23:38:04 2014 -0400

    Add test case for CANCEL iTip messages

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index ea84cc4..e438343 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -433,6 +433,9 @@ class Event(object):
     def get_sequence(self):
         return self.event.sequence()
 
+    def get_transparency(self):
+        return self.event.transparency()
+
     def set_attendee_participant_status(self, attendee, status):
         """
             Set the participant status of an attendee to status.
@@ -664,6 +667,9 @@ class Event(object):
     def set_uid(self, uid):
         self.event.setUid(str(uid))
 
+    def set_transparency(self, transp):
+        return self.event.setTransparency(transp)
+
     def __str__(self):
         event_xml = kolabformat.writeEvent(self.event)
 
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 0490ec1..10a377f 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -159,7 +159,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'dn': 'uid=manager,ou=People,dc=example,dc=org',
             'mailbox': 'user/jane.manager at example.org',
             'kolabtargetfolder': 'user/jane.manager/Calendar at example.org',
-            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT']
+            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT', 'ACT_UPDATE']
         }
 
         from tests.functional.user_add import user_add
@@ -240,16 +240,19 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             },
             mailto,
             attendee_email,
-            method="REPLY")
+            method='REPLY')
 
         return uid
 
-    def send_itip_cancel(self, resource_email, uid):
-        self.send_message(itip_cancellation % (
-                uid,
-                resource_email
-            ),
-            resource_email)
+    def send_itip_cancel(self, attendee_email, uid, summary="test", sequence=1):
+        self.send_message(itip_cancellation % {
+                'uid': uid,
+                'mailto': attendee_email,
+                'summary': summary,
+                'sequence': sequence,
+            },
+            attendee_email,
+            method='CANCEL')
 
         return uid
 
@@ -446,4 +449,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         attendee = event.get_attendee(self.jane['mail'])
         self.assertIsInstance(attendee, pykolab.xml.Attendee)
         self.assertEqual(attendee.get_participant_status(), kolabformat.PartAccepted)
+
+    def test_005_invitation_cancel(self):
+        uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled")
+
+        response = self.check_message_received('"cancelled" has been ACCEPTED', self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
+
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "cancelled")
+        self.assertEqual(event.get_status(), 'CANCELLED')
+        self.assertTrue(event.get_transparency())
+
         
\ No newline at end of file
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index b5863c2..d4ed7d5 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -303,7 +303,8 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
     nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant
     partstat = receiving_attendee.get_participant_status()
     save_event = not nonpart or not partstat == kolabformat.PartNeedsAction
-    scheduling_required = receiving_attendee.get_rsvp() or partstat == kolabformat.PartNeedsAction
+    rsvp = receiving_attendee.get_rsvp()
+    scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction
     condition_fulfilled = True
 
     # find existing event in user's calendar
@@ -325,7 +326,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
 
     # if RSVP, send an iTip REPLY
-    if scheduling_required:
+    if rsvp or scheduling_required:
         respond_with = None
         if policy & ACT_ACCEPT and condition_fulfilled:
             respond_with = 'TENTATIVE' if policy & MOD_TENTATIVE else 'ACCEPTED'
@@ -348,6 +349,10 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             send_reply(recipient_email, itip_event, invitation_response_text(),
                 subject=_('"%(summary)s" has been %(status)s'))
 
+        # elif partstat == kolabformat.PartNeedsAction and conf.get('wallace','invitationpolicy_always_copy_to_calendar'):
+            # TODO: copy the invitation into the user's calendar with unchanged PARTSTAT
+            # TODO: or use ACT_POSTPONE for this?
+
         else:
             # policy doesn't match, pass on to next one
             return None
@@ -407,8 +412,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
                 return MESSAGE_FORWARD
 
             # update the organizer's copy of the event
-            delete_event(existing)
-            if store_event(existing, receiving_user, existing._imap_folder):
+            if update_event(existing, receiving_user):
                 # TODO: send (consolidated) notification to organizer if policy & ACT_UPDATE_AND_NOTIFY:
                 # TODO: update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
                 return MESSAGE_PROCESSED
@@ -430,9 +434,23 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
         log.info(_("Pass cancellation for manual processing"))
         return MESSAGE_FORWARD
 
-    # update_event_in_user_calendar(itip_event, receiving_user)
+    # auto-update the local copy with STATUS=CANCELLED
+    if policy & ACT_UPDATE:
+        # find existing event in user's calendar
+        existing = find_existing_event(itip_event, receiving_user)
+
+        if existing:
+            existing.set_status('CANCELLED')
+            existing.set_transparency(True)
+            if update_event(existing, receiving_user):
+                # TODO: send cancellation notification if policy & ACT_UPDATE_AND_NOTIFY: ?
+                return MESSAGE_PROCESSED
+
+        else:
+            log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+            return MESSAGE_FORWARD
 
-    return MESSAGE_PROCESSED
+    return None
 
 
 def user_dn_from_email_address(email_address):
@@ -659,6 +677,17 @@ def check_availability(itip_event, receiving_user):
     return not conflict
 
 
+def update_event(event, user_rec):
+    """
+        Update the given event in IMAP (i.e. delete + append)
+    """
+    if hasattr(event, '_imap_folder'):
+        delete_event(event)
+        return store_event(event, user_rec, event._imap_folder)
+
+    return False
+
+
 def store_event(event, user_rec, targetfolder=None):
     """
         Append the given event object to the user's default calendar


commit 014b466cf3bdd6d4621b7a8b99dcae912175693e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 23:27:40 2014 -0400

    Make sure the 'attendees' property of an iTip event always is a list

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index 04b2d55..42e08dd 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -100,6 +100,9 @@ def objects_from_message(message, objname, methods=None):
 
                     itip['attendees'] = c['attendee']
 
+                    if itip.has_key('attendee') and not isinstance(itip['attendees'], list):
+                        itip['attendees'] = [c['attendee']]
+
                     if c.has_key('resources'):
                         itip['resources'] = c['resources']
 


commit ce4be6aec8a5112ead076f7a2c6a8ad7eeb403e6
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 22:14:32 2014 -0400

    Start implementing a new wallace module 'invitationpolicy' to automatically process iTip messages according to per-user policies

diff --git a/conf/kolab.conf b/conf/kolab.conf
index 2f8ea2b..cb3a7ba 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -361,10 +361,13 @@ admin_password = Welcome123
 result_attribute = mail
 
 [wallace]
-modules = resources, footer
+modules = resources, invitationpolicy, footer
 footer_text = /etc/kolab/footer.text
 footer_html = /etc/kolab/footer.html
 
+; default settings for kolabInvitationPolicy
+kolab_invitation_policy = ACT_ACCEPT_IF_NO_CONFLICT:example.org, ACT_MANUAL
+
 ; This is a domain name space specific section, that enables us to override
 ; all settings, for example, the LDAP URI, base and bind DNs, scopes, filters,
 ; etc. Note that overriding the LDAP settings for the primary domain name space
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
new file mode 100644
index 0000000..0490ec1
--- /dev/null
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -0,0 +1,449 @@
+import time
+import pykolab
+import smtplib
+import email
+import datetime
+import pytz
+import uuid
+import kolabformat
+
+from pykolab.imap import IMAP
+from wallace import module_resources
+
+from email import message_from_string
+from twisted.trial import unittest
+
+import tests.functional.resource_func as funcs
+
+conf = pykolab.getConf()
+
+itip_invitation = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTAMP:20140213T1254140
+DTSTART;TZID=Europe/Berlin:%(start)s
+DTEND;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=%(partstat)s;RSVP=TRUE:mailto:%(mailto)s
+ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=TENTATIVE;RSVP=FALSE:mailto:somebody at else.com
+TRANSP:OPAQUE
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_cancellation = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTAMP:20140218T1254140
+DTSTART;TZID=Europe/Berlin:20120713T100000
+DTEND;TZID=Europe/Berlin:20120713T110000
+SUMMARY:%(summary)s
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE:mailto:%(mailto)s
+TRANSP:OPAQUE
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_recurring = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTAMP:20140213T1254140
+DTSTART;TZID=Europe/Zurich:%(start)s
+DTEND;TZID=Europe/Zurich:%(end)s
+RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
+SUMMARY:%(summary)s
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=%(partstat)s;RSVP=TRUE:mailto:%(mailto)s
+TRANSP:OPAQUE
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_reply = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//pykolab-0.6.9-1//kolab.org//
+CALSCALE:GREGORIAN
+METHOD:REPLY
+BEGIN:VEVENT
+SUMMARY:%(summary)s
+UID:%(uid)s
+DTSTART;TZID=Europe/Berlin;VALUE=DATE-TIME:%(start)s
+DTEND;TZID=Europe/Berlin;VALUE=DATE-TIME:%(end)s
+DTSTAMP;VALUE=DATE-TIME:20140706T171038Z
+ORGANIZER;CN="Doe, John":MAILTO:%(organizer)s
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+PRIORITY:0
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+mime_message = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe at example.org>
+Date: Tue, 25 Feb 2014 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7 at example.org>
+To: %s
+Subject: "test"
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=%s; name=event.ics
+Content-Disposition: attachment; filename=event.ics
+Content-Transfer-Encoding: 8bit
+
+%s
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+class TestWallaceInvitationpolicy(unittest.TestCase):
+
+    john = None
+
+    @classmethod
+    def setUp(self):
+        """ Compatibility for twisted.trial.unittest
+        """
+        if not self.john:
+            self.setup_class()
+
+    @classmethod
+    def setup_class(self, *args, **kw):
+        from tests.functional.purge_users import purge_users
+        purge_users()
+
+        self.john = {
+            'displayname': 'John Doe',
+            'mail': 'john.doe at example.org',
+            'sender': 'John Doe <john.doe at example.org>',
+            'dn': 'uid=doe,ou=People,dc=example,dc=org',
+            'mailbox': 'user/john.doe at example.org',
+            'kolabtargetfolder': 'user/john.doe/Calendar at example.org',
+            'kolabinvitationpolicy': ['ACT_UPDATE', 'ACT_MANUAL']
+        }
+
+        self.jane = {
+            'displayname': 'Jane Manager',
+            'mail': 'jane.manager at example.org',
+            'sender': 'Jane Manager <jane.manager at example.org>',
+            'dn': 'uid=manager,ou=People,dc=example,dc=org',
+            'mailbox': 'user/jane.manager at example.org',
+            'kolabtargetfolder': 'user/jane.manager/Calendar at example.org',
+            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT']
+        }
+
+        from tests.functional.user_add import user_add
+        user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'])
+        user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'])
+
+        time.sleep(1)
+        from tests.functional.synchronize import synchronize_once
+        synchronize_once()
+
+    def send_message(self, itip_payload, to_addr, from_addr=None, method="REQUEST"):
+        if from_addr is None:
+            from_addr = self.john['mail']
+
+        smtp = smtplib.SMTP('localhost', 10026)
+        smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, method, itip_payload))
+
+    def send_itip_invitation(self, attendee_email, start=None, allday=False, template=None, summary="test", sequence=0, partstat='NEEDS-ACTION'):
+        if start is None:
+            start = datetime.datetime.now()
+
+        uid = str(uuid.uuid4())
+
+        if allday:
+            default_template = itip_allday
+            end = start + datetime.timedelta(days=1)
+            date_format = '%Y%m%d'
+        else:
+            end = start + datetime.timedelta(hours=4)
+            default_template = itip_invitation
+            date_format = '%Y%m%dT%H%M%S'
+
+        self.send_message((template if template is not None else default_template) % {
+                'uid': uid,
+                'start': start.strftime(date_format),
+                'end': end.strftime(date_format),
+                'mailto': attendee_email,
+                'summary': summary,
+                'sequence': sequence,
+                'partstat': partstat
+            },
+            attendee_email)
+
+        return uid
+
+    def send_itip_update(self, attendee_email, uid, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'):
+        if start is None:
+            start = datetime.datetime.now()
+
+        end = start + datetime.timedelta(hours=4)
+        self.send_message((template if template is not None else itip_invitation) % {
+                'uid': uid,
+                'start': start.strftime('%Y%m%dT%H%M%S'),
+                'end': end.strftime('%Y%m%dT%H%M%S'),
+                'mailto': attendee_email,
+                'summary': summary,
+                'sequence': sequence,
+                'partstat': partstat
+            },
+            attendee_email)
+
+        return uid
+
+    def send_itip_reply(self, uid, mailto, attendee_email, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'):
+        if start is None:
+            start = datetime.datetime.now()
+
+        end = start + datetime.timedelta(hours=4)
+        self.send_message((template if template is not None else itip_reply) % {
+                'uid': uid,
+                'start': start.strftime('%Y%m%dT%H%M%S'),
+                'end': end.strftime('%Y%m%dT%H%M%S'),
+                'mailto': attendee_email,
+                'organizer': mailto,
+                'summary': summary,
+                'sequence': sequence,
+                'partstat': partstat
+            },
+            mailto,
+            attendee_email,
+            method="REPLY")
+
+        return uid
+
+    def send_itip_cancel(self, resource_email, uid):
+        self.send_message(itip_cancellation % (
+                uid,
+                resource_email
+            ),
+            resource_email)
+
+        return uid
+
+    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendee=None):
+        if start is None:
+            start = datetime.datetime.now(pytz.timezone("Europe/Berlin"))
+        if user is None:
+            user = self.john
+        if attendee is None:
+            attendee = self.jane
+
+        end = start + datetime.timedelta(hours=4)
+
+        event = pykolab.xml.Event()
+        event.set_start(start)
+        event.set_end(end)
+        event.set_organizer(user['mail'], user['displayname'])
+        event.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+        event.set_summary(summary)
+        event.set_sequence(sequence)
+
+        imap = IMAP()
+        imap.connect()
+
+        mailbox = imap.folder_quote(user['kolabtargetfolder'])
+        imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+        imap.imap.m.select(mailbox)
+
+        result = imap.imap.m.append(
+            mailbox,
+            None,
+            None,
+            event.to_message().as_string()
+        )
+
+        return event.get_uid()
+
+    def check_message_received(self, subject, from_addr=None, mailbox=None):
+        if mailbox is None:
+            mailbox = self.john['mailbox']
+
+        imap = IMAP()
+        imap.connect()
+
+        mailbox = imap.folder_quote(mailbox)
+        imap.set_acl(mailbox, "cyrus-admin", "lrs")
+        imap.imap.m.select(mailbox)
+
+        found = None
+        retries = 15
+
+        while not found and retries > 0:
+            retries -= 1
+
+            typ, data = imap.imap.m.search(None, '(UNDELETED HEADER FROM "%s")' % (from_addr) if from_addr else 'UNDELETED')
+            for num in data[0].split():
+                typ, msg = imap.imap.m.fetch(num, '(RFC822)')
+                message = message_from_string(msg[0][1])
+                if message['Subject'] == subject:
+                    found = message
+                    break
+
+            time.sleep(1)
+
+        imap.disconnect()
+
+        return found
+
+    def check_user_calendar_event(self, mailbox, uid=None):
+        imap = IMAP()
+        imap.connect()
+
+        mailbox = imap.folder_quote(mailbox)
+        imap.set_acl(mailbox, "cyrus-admin", "lrs")
+        imap.imap.m.select(mailbox)
+
+        found = None
+        retries = 15
+
+        while not found and retries > 0:
+            retries -= 1
+
+            typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")')
+            for num in data[0].split():
+                typ, data = imap.imap.m.fetch(num, '(RFC822)')
+                event_message = message_from_string(data[0][1])
+
+                # return matching UID or first event found
+                if uid and event_message['subject'] != uid:
+                    continue
+
+                for part in event_message.walk():
+                    if part.get_content_type() == "application/calendar+xml":
+                        payload = part.get_payload(decode=True)
+                        found = pykolab.xml.event_from_string(payload)
+                        break
+
+                if found:
+                    break
+
+            time.sleep(1)
+
+        return found
+
+    def purge_mailbox(self, mailbox):
+        imap = IMAP()
+        imap.connect()
+        mailbox = imap.folder_quote(mailbox)
+        imap.set_acl(mailbox, "cyrus-admin", "lrwcdest")
+        imap.imap.m.select(mailbox)
+
+        typ, data = imap.imap.m.search(None, 'ALL')
+        for num in data[0].split():
+            imap.imap.m.store(num, '+FLAGS', '\\Deleted')
+
+        imap.imap.m.expunge()
+        imap.disconnect()
+
+
+    def test_001_invite_user(self):
+        start = datetime.datetime(2014,8,13, 10,0,0)
+        uid = self.send_itip_invitation(self.jane['mail'], start)
+
+        response = self.check_message_received('"test" has been ACCEPTED', self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test")
+
+        # send update with the same sequence: no re-scheduling
+        self.send_itip_update(self.jane['mail'], uid, start, summary="test updated", sequence=0, partstat='ACCEPTED')
+
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test updated")
+        self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+
+    # @depends on test_001_invite_user
+    def test_002_invite_conflict(self):
+        uid = self.send_itip_invitation(self.jane['mail'], datetime.datetime(2014,8,13, 11,0,0), summary="test2")
+
+        response = self.check_message_received('"test2" has been DECLINED', self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test2")
+
+
+    def test_003_invite_rescheduling(self):
+        start = datetime.datetime(2014,8,14, 9,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.send_itip_invitation(self.jane['mail'], start)
+
+        response = self.check_message_received('"test" has been ACCEPTED', self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test")
+
+        self.purge_mailbox(self.john['mailbox'])
+
+        # send update with new date and incremented sequence
+        new_start = datetime.datetime(2014,8,15, 15,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        self.send_itip_update(self.jane['mail'], uid, new_start, summary="test", sequence=1)
+
+        response = self.check_message_received('"test" has been ACCEPTED', self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_start(), new_start)
+        self.assertEqual(event.get_sequence(), 1)
+
+
+    def test_004_invitation_reply(self):
+        start = datetime.datetime(2014,8,18, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.john)
+
+        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+
+        # send a reply from jane to john
+        self.send_itip_reply(uid, self.john['mail'], self.jane['mail'], start=start)
+
+        # check for the updated event in john's calendar
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+
+        attendee = event.get_attendee(self.jane['mail'])
+        self.assertIsInstance(attendee, pykolab.xml.Attendee)
+        self.assertEqual(attendee.get_participant_status(), kolabformat.PartAccepted)
+        
\ No newline at end of file
diff --git a/tests/functional/user_add.py b/tests/functional/user_add.py
index 4939f93..b1b37f1 100644
--- a/tests/functional/user_add.py
+++ b/tests/functional/user_add.py
@@ -4,7 +4,7 @@ from pykolab import wap_client
 
 conf = pykolab.getConf()
 
-def user_add(givenname, sn, preferredlanguage='en_US'):
+def user_add(givenname, sn, preferredlanguage='en_US', **kw):
     if givenname == None:
         raise Exception
 
@@ -25,6 +25,8 @@ def user_add(givenname, sn, preferredlanguage='en_US'):
             'userpassword': 'Welcome2KolabSystems'
         }
 
+    user_details.update(kw)
+
     login = conf.get('ldap', 'bind_dn')
     password = conf.get('ldap', 'bind_pw')
     domain = conf.get('kolab', 'primary_domain')
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
new file mode 100644
index 0000000..75939d0
--- /dev/null
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -0,0 +1,129 @@
+import pykolab
+import logging
+import datetime
+
+from icalendar import Calendar
+from email import message
+from email import message_from_string
+from wallace import module_invitationpolicy as MIP
+from twisted.trial import unittest
+
+from pykolab.auth.ldap import LDAP
+from pykolab.constants import *
+
+
+# define some iTip MIME messages
+
+itip_multipart = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe at example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7 at example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: jane.doe at example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Disposition: attachment;
+ filename=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 1.0.1//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe at example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:jane.doe at example.org
+ATTENDEE;ROLE=3DOPT-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+user.external at example.com
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+conf = pykolab.getConf()
+
+if not hasattr(conf, 'defaults'):
+    conf.finalize_conf()
+
+class TestWallaceInvitationpolicy(unittest.TestCase):
+
+    def setUp(self):
+        # monkey-patch the pykolab.auth module to check API calls
+        # without actually connecting to LDAP
+        #self.patch(pykolab.auth.Auth, "connect", self._mock_nop)
+        #self.patch(pykolab.auth.Auth, "disconnect", self._mock_nop)
+        #self.patch(pykolab.auth.Auth, "find_user_dn", self._mock_find_user_dn)
+        #self.patch(pykolab.auth.Auth, "get_entry_attributes", self._mock_get_entry_attributes)
+        #self.patch(pykolab.auth.Auth, "search_entry_by_attribute", self._mock_search_entry_by_attribute)
+
+        # intercept calls to smtplib.SMTP.sendmail()
+        import smtplib
+        self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init)
+        self.patch(smtplib.SMTP, "quit", self._mock_nop)
+        self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail)
+
+        self.smtplog = [];
+
+    def _mock_find_user_dn(self, value, kolabuser=False):
+        (prefix, domain) = value.split('@')
+        return "uid=" + prefix + ",ou=People,dc=" + ",dc=".join(domain.split('.'))
+
+    def _mock_get_entry_attributes(self, domain, entry, attributes):
+        (_, uid) = entry.split(',')[0].split('=')
+        return { 'cn': uid, 'mail': uid + "@example.org", '_attrib': attributes }
+
+    def _mock_nop(self, domain=None):
+        pass
+
+    def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
+        pass
+
+    def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None):
+        self.smtplog.append((from_addr, to_addr, message))
+
+    def test_001_itip_events_from_message(self):
+        itips = pykolab.itip.events_from_message(message_from_string(itip_multipart))
+        self.assertEqual(len(itips), 1, "Multipart iTip message with text/calendar")
+        self.assertEqual(itips[0]['method'], "REQUEST", "iTip request method property")
+        self.assertEqual(len(itips[0]['attendees']), 2, "List attendees from iTip")
+        self.assertEqual(itips[0]['attendees'][0], "mailto:jane.doe at example.org", "First attendee from iTip")
+
+    def test_002_user_dn_from_email_address(self):
+        res = MIP.user_dn_from_email_address("doe at example.org")
+        # assert call to (patched) pykolab.auth.Auth.find_resource()
+        self.assertEqual("uid=doe,ou=People,dc=example,dc=org", res);
+
+    def test_003_get_matching_invitation_policy(self):
+        user = { 'kolabinvitationpolicy': [
+            'ACT_ACCEPT:example.org',
+            'ACT_REJECT:gmail.com',
+            'ACT_MANUAL:*'
+        ] }
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'fastmail.net'), [MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'example.org'),  [MIP.ACT_ACCEPT,MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'gmail.com'),    [MIP.ACT_REJECT,MIP.ACT_MANUAL])
+
+        user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] }
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'somedomain.net'), [MIP.ACT_MANUAL])
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
new file mode 100644
index 0000000..b5863c2
--- /dev/null
+++ b/wallace/module_invitationpolicy.py
@@ -0,0 +1,721 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 Kolab Systems AG (http://www.kolabsys.com)
+#
+# Thomas Bruederli (Kolab Systems) <bruederli at kolabsys.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+import datetime
+import os
+import tempfile
+import time
+from urlparse import urlparse
+import urllib
+
+from email import message_from_string
+from email.parser import Parser
+from email.utils import formataddr
+from email.utils import getaddresses
+
+import modules
+
+import pykolab
+import kolabformat
+
+from pykolab.auth import Auth
+from pykolab.conf import Conf
+from pykolab.imap import IMAP
+from pykolab.xml import to_dt
+from pykolab.xml import event_from_string
+from pykolab.itip import events_from_message
+from pykolab.itip import check_event_conflict
+from pykolab.itip import send_reply
+from pykolab.translate import _
+
+# define some contstants used in the code below
+MOD_IF_AVAILABLE   = 32
+MOD_IF_CONFLICT    = 64
+MOD_TENTATIVE      = 128
+MOD_NOTIFY         = 256
+ACT_MANUAL         = 1
+ACT_ACCEPT         = 2
+ACT_DELEGATE       = 4
+ACT_REJECT         = 8
+ACT_UPDATE         = 16
+ACT_TENTATIVE                = ACT_ACCEPT + MOD_TENTATIVE
+ACT_ACCEPT_IF_NO_CONFLICT    = ACT_ACCEPT + MOD_IF_AVAILABLE
+ACT_TENTATIVE_IF_NO_CONFLICT = ACT_ACCEPT + MOD_TENTATIVE + MOD_IF_AVAILABLE
+ACT_DELEGATE_IF_CONFLICT     = ACT_DELEGATE + MOD_IF_CONFLICT
+ACT_REJECT_IF_CONFLICT       = ACT_REJECT + MOD_IF_CONFLICT
+ACT_UPDATE_AND_NOTIFY        = ACT_UPDATE + MOD_NOTIFY
+
+FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
+
+MESSAGE_PROCESSED = 1
+MESSAGE_FORWARD   = 2
+
+policy_name_map = {
+    'ACT_MANUAL':                   ACT_MANUAL,
+    'ACT_ACCEPT':                   ACT_ACCEPT,
+    'ACT_ACCEPT_IF_NO_CONFLICT':    ACT_ACCEPT_IF_NO_CONFLICT,
+    'ACT_TENTATIVE':                ACT_TENTATIVE,
+    'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_TENTATIVE_IF_NO_CONFLICT,
+    'ACT_DELEGATE':                 ACT_DELEGATE,
+    'ACT_DELEGATE_IF_CONFLICT':     ACT_DELEGATE_IF_CONFLICT,
+    'ACT_REJECT':                   ACT_REJECT,
+    'ACT_REJECT_IF_CONFLICT':       ACT_REJECT_IF_CONFLICT,
+    'ACT_UPDATE':                   ACT_UPDATE,
+    'ACT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY
+}
+
+policy_value_map = dict([(v, k) for (k, v) in policy_name_map.iteritems()])
+
+log = pykolab.getLogger('pykolab.wallace')
+conf = pykolab.getConf()
+
+mybasepath = '/var/spool/pykolab/wallace/invitationpolicy/'
+
+auth = None
+imap = None
+
+def __init__():
+    modules.register('invitationpolicy', execute, description=description())
+
+def accept(filepath):
+    new_filepath = os.path.join(
+        mybasepath,
+        'ACCEPT',
+        os.path.basename(filepath)
+    )
+
+    cleanup()
+    os.rename(filepath, new_filepath)
+    filepath = new_filepath
+    exec('modules.cb_action_ACCEPT(%r, %r)' % ('invitationpolicy',filepath))
+
+def reject(filepath):
+    new_filepath = os.path.join(
+        mybasepath,
+        'REJECT',
+        os.path.basename(filepath)
+    )
+
+    os.rename(filepath, new_filepath)
+    filepath = new_filepath
+    exec('modules.cb_action_REJECT(%r, %r)' % ('invitationpolicy',filepath))
+
+def description():
+    return """Invitation policy execution module."""
+
+def cleanup():
+    global auth, imap
+
+    log.debug("cleanup(): %r, %r" % (auth, imap), level=9)
+
+    auth.disconnect()
+    del auth
+
+    # Disconnect IMAP or we lock the mailbox almost constantly
+    imap.disconnect()
+    del imap
+
+def execute(*args, **kw):
+    global auth, imap
+
+    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(_("Invitation policy called for %r, %r") % (args, kw), level=9)
+
+    auth = Auth()
+    imap = IMAP()
+
+    # TODO: Test for correct call.
+    filepath = args[0]
+
+    if kw.has_key('stage'):
+        log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8)
+
+        log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8)
+        if hasattr(modules, 'cb_action_%s' % (kw['stage'])):
+            log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8)
+
+            exec(
+                'modules.cb_action_%s(%r, %r)' % (
+                    kw['stage'],
+                    'invitationpolicy',
+                    filepath
+                )
+            )
+
+            return filepath
+    else:
+        # Move to incoming
+        new_filepath = os.path.join(
+            mybasepath,
+            'incoming',
+            os.path.basename(filepath)
+        )
+
+        if not filepath == new_filepath:
+            log.debug("Renaming %r to %r" % (filepath, new_filepath))
+            os.rename(filepath, new_filepath)
+            filepath = new_filepath
+
+    # parse full message
+    message = Parser().parse(open(filepath, 'r'))
+
+    recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))]
+    sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0]
+
+    any_itips = False
+    recipient_email = None
+    recipient_user_dn = None
+
+    # An iTip message may contain multiple events. Later on, test if the message
+    # is an iTip message by checking the length of this list.
+    try:
+        itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL'])
+    except Exception, e:
+        log.error(_("Failed to parse iTip events from message: %r" % (e)))
+        itip_events = []
+
+    if not len(itip_events) > 0:
+        log.info(_("Message is not an iTip message or does not contain any (valid) iTip events."))
+
+    else:
+        any_itips = True
+        log.debug(_("iTip events attached to this message contain the following information: %r") % (itip_events), level=9)
+
+    # See if any iTip actually allocates a user.
+    if any_itips and len([x['uid'] for x in itip_events if x.has_key('attendees') or x.has_key('organizer')]) > 0:
+        auth.connect()
+
+        for recipient in recipients:
+            recipient_user_dn = user_dn_from_email_address(recipient)
+            if recipient_user_dn is not None:
+                recipient_email = recipient
+                break
+
+    if not any_itips:
+        log.debug(_("No itips, no users, pass along %r") % (filepath), level=5)
+        return filepath
+    elif recipient_email is None:
+        log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5)
+        return filepath
+
+    # we're looking at the first itip event object
+    itip_event = itip_events[0];
+
+    # for replies, the organizer is the recipient
+    if itip_event['method'] == 'REPLY':
+        user_attendees = [itip_event['organizer']] if str(itip_event['organizer']).split(':')[-1] == recipient_email else []
+
+    else:
+        # Limit the attendees to the one that is actually invited with the current message.
+        attendees = [str(a).split(':')[-1] for a in (itip_event['attendees'] if itip_event.has_key('attendees') else [])]
+        user_attendees = [a for a in attendees if a == recipient_email]
+
+        if itip_event.has_key('organizer'):
+            sender_email = itip_event['xml'].get_organizer().email()
+
+    # abort if no attendee matches the envelope recipient
+    if len(user_attendees) == 0:
+        log.info(_("No user attendee matching envelope recipient %s, skip message") % (recipient_email))
+        return filepath
+
+    receiving_user = auth.get_entry_attributes(None, recipient_user_dn, ['*'])
+    log.debug(_("Receiving user: %r") % (receiving_user), level=9)
+
+    # find user's kolabInvitationPolicy settings and the matching policy values
+    sender_domain = str(sender_email).split('@')[-1]
+    policies = get_matching_invitation_policies(receiving_user, sender_domain)
+
+    # select a processing function according to the iTip request method
+    method_processing_map = {
+        'REQUEST': process_itip_request,
+        'REPLY':   process_itip_reply,
+        'CANCEL':  process_itip_cancel
+    }
+
+    done = None
+    if method_processing_map.has_key(itip_event['method']):
+        processor_func = method_processing_map[itip_event['method']]
+
+        # connect as cyrus-admin
+        imap.connect()
+
+        for policy in policies:
+            log.debug(_("Apply invitation policy %r for domain %r") % (policy_value_map[policy], sender_domain), level=8)
+            done = processor_func(itip_event, policy, recipient_email, sender_email, receiving_user)
+
+            # matching policy found
+            if done is not None:
+                break
+
+    else:
+        log.debug(_("Ignoring '%s' iTip method") % (itip_event['method']), level=8)
+
+    # message has been processed by the module, remove it
+    if done == MESSAGE_PROCESSED:
+        log.debug(_("iTip message %r consumed by the invitationpolicy module") % (message.get('Message-ID')), level=5)
+        os.unlink(filepath)
+        filepath = None
+
+    cleanup()
+    return filepath
+
+
+def process_itip_request(itip_event, policy, recipient_email, sender_email, receiving_user):
+    """
+        Process an iTip REQUEST message according to the given policy
+    """
+
+    # if invitation policy is set to MANUAL, pass message along
+    if policy & ACT_MANUAL:
+        log.info(_("Pass invitation for manual processing"))
+        return MESSAGE_FORWARD
+
+    try:
+        receiving_attendee = itip_event['xml'].get_attendee_by_email(recipient_email)
+        log.debug(_("Receiving Attendee: %r") % (receiving_attendee), level=9)
+    except Exception, e:
+        log.error("Could not find envelope attendee: %r" % (e))
+        return MESSAGE_FORWARD
+
+    # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
+    nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant
+    partstat = receiving_attendee.get_participant_status()
+    save_event = not nonpart or not partstat == kolabformat.PartNeedsAction
+    scheduling_required = receiving_attendee.get_rsvp() or partstat == kolabformat.PartNeedsAction
+    condition_fulfilled = True
+
+    # find existing event in user's calendar
+    existing = find_existing_event(itip_event, receiving_user)
+
+    # compare sequence number to determine a (re-)scheduling request
+    if existing is not None:
+        log.debug(_("Existing event: %r") % (existing), level=9)
+        scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] >= existing.get_sequence()
+        save_event = True
+
+    # if scheduling: check availability
+    if scheduling_required:
+        if policy & (MOD_IF_AVAILABLE | MOD_IF_CONFLICT):
+            condition_fulfilled = check_availability(itip_event, receiving_user)
+        if policy & MOD_IF_CONFLICT:
+            condition_fulfilled = not condition_fulfilled
+
+        log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
+
+    # if RSVP, send an iTip REPLY
+    if scheduling_required:
+        respond_with = None
+        if policy & ACT_ACCEPT and condition_fulfilled:
+            respond_with = 'TENTATIVE' if policy & MOD_TENTATIVE else 'ACCEPTED'
+
+        elif policy & ACT_REJECT and condition_fulfilled:
+            respond_with = 'DECLINED'
+            # TODO: only save declined invitation when a certain config option is set?
+
+        elif policy & ACT_DELEGATE and condition_fulfilled:
+            # TODO: save and delegate (but to whom?)
+            pass
+
+        # send iTip reply
+        if respond_with is not None:
+            # set attendee's CN from LDAP record if yet missing
+            if not receiving_attendee.get_name() and receiving_user.has_key('cn'):
+                receiving_attendee.set_name(receiving_user['cn'])
+
+            receiving_attendee.set_participant_status(respond_with)
+            send_reply(recipient_email, itip_event, invitation_response_text(),
+                subject=_('"%(summary)s" has been %(status)s'))
+
+        else:
+            # policy doesn't match, pass on to next one
+            return None
+
+    else:
+        log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
+        # TODO: only update if policy & ACT_UPDATE ?
+
+    if save_event:
+        targetfolder = None
+
+        if existing:
+            # delete old version from IMAP
+            targetfolder = existing._imap_folder
+            delete_event(existing)
+
+        if not nonpart or existing:
+            # save new copy from iTip
+            if store_event(itip_event['xml'], receiving_user, targetfolder):
+                return MESSAGE_PROCESSED
+
+    return None
+
+
+def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiving_user):
+    """
+        Process an iTip REPLY message according to the given policy
+    """
+
+    # if invitation policy is set to MANUAL, pass message along
+    if policy & ACT_MANUAL:
+        log.info(_("Pass reply for manual processing"))
+        return MESSAGE_FORWARD
+
+    # auto-update is enabled for this user
+    if policy & ACT_UPDATE:
+        try:
+            sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email)
+            log.debug(_("Sender Attendee: %r") % (sender_attendee), level=9)
+        except Exception, e:
+            log.error("Could not find envelope sender attendee: %r" % (e))
+            return MESSAGE_FORWARD
+
+        # find existing event in user's calendar
+        existing = find_existing_event(itip_event, receiving_user)
+
+        if existing:
+            log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
+
+            # TODO: compare sequence number to avoid outdated replies?
+            try:
+                existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status())
+            except Exception, e:
+                log.error("Could not find corresponding attende in organizer's event: %r" % (e))
+
+                # TODO: accept new participant if ACT_ACCEPT ?
+                return MESSAGE_FORWARD
+
+            # update the organizer's copy of the event
+            delete_event(existing)
+            if store_event(existing, receiving_user, existing._imap_folder):
+                # TODO: send (consolidated) notification to organizer if policy & ACT_UPDATE_AND_NOTIFY:
+                # TODO: update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
+                return MESSAGE_PROCESSED
+
+        else:
+            log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+            return MESSAGE_FORWARD
+
+    return None
+
+
+def process_itip_cancel(itip_event, policy, recipient_email, sender_email, receiving_user):
+    """
+        Process an iTip CANCEL message according to the given policy
+    """
+
+    # if invitation policy is set to MANUAL, pass message along
+    if policy & ACT_MANUAL:
+        log.info(_("Pass cancellation for manual processing"))
+        return MESSAGE_FORWARD
+
+    # update_event_in_user_calendar(itip_event, receiving_user)
+
+    return MESSAGE_PROCESSED
+
+
+def user_dn_from_email_address(email_address):
+    """
+        Resolves the given email address to a Kolab user entity
+    """
+    global auth
+
+    if not auth:
+        auth = Auth()
+        auth.connect()
+
+    local_domains = auth.list_domains()
+
+    if not local_domains == None:
+        local_domains = list(set(local_domains.keys()))
+
+    if not email_address.split('@')[1] in local_domains:
+        return None
+
+    log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8)
+
+    user_dn = auth.find_user_dn(email_address, True)
+
+    if isinstance(user_dn, basestring):
+        log.debug(_("User DN: %r") % (user_dn), level=8)
+    else:
+        log.debug(_("No user record(s) found for %r") % (email_address), level=9)
+
+    auth.disconnect()
+
+    return user_dn
+
+
+def get_matching_invitation_policies(receiving_user, sender_domain):
+    # get user's kolabInvitationPolicy settings
+    policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else []
+    if policies and not isinstance(policies, list):
+        policies = [policies]
+
+    if len(policies) == 0:
+        policies = conf.get_list('wallace', 'kolab_invitation_policy')
+
+    # match policies agains the given sender_domain
+    matches = []
+    for p in policies:
+        if ':' in p:
+            (value, domain) = p.split(':')
+        else:
+            value = p
+            domain = ''
+
+        if domain == '' or domain == '*' or sender_domain.endswith(domain):
+            value = value.upper()
+            if policy_name_map.has_key(value):
+                matches.append(policy_name_map[value])
+
+    # add manual as default action
+    if len(matches) == 0:
+        matches.append(ACT_MANUAL)
+
+    return matches
+
+
+def imap_proxy_auth(user_rec):
+    """
+        
+    """
+    global imap
+
+    mail_attribute = conf.get('cyrus-sasl', 'result_attribute')
+    if mail_attribute == None:
+        mail_attribute = 'mail'
+
+    mail_attribute = mail_attribute.lower()
+
+    if not user_rec.has_key(mail_attribute):
+        log.error(_("User record doesn't have the mailbox attribute %r set" % (mail_attribute)))
+        return False
+
+    # do IMAP prox auth with the given user
+    backend = conf.get('kolab', 'imap_backend')
+    admin_login = conf.get(backend, 'admin_login')
+    admin_password = conf.get(backend, 'admin_password')
+
+    try:
+        imap.disconnect()
+        imap.connect(login=False)
+        imap.login_plain(admin_login, admin_password, user_rec[mail_attribute])
+    except Exception, errmsg:
+        log.error(_("IMAP proxy authentication failed: %r") % (errmsg))
+        return False
+
+    return True
+
+
+def list_user_calendars(user_rec):
+    """
+        Get a list of the given user's private calendar folders
+    """
+    global imap
+
+    # return cached list
+    if user_rec.has_key('_calendar_folders'):
+        return user_rec['_calendar_folders'];
+
+    calendars = []
+
+    if not imap_proxy_auth(user_rec):
+        return calendars
+
+    folders = imap.list_folders('*')
+    log.debug(_("List calendar folders for user %r: %r") % (user_rec['mail'], folders), level=8)
+
+    (ns_personal, ns_other, ns_shared) = imap.namespaces()
+
+    if isinstance(ns_shared, list):
+        ns_shared = ns_shared[0]
+    if isinstance(ns_other, list):
+        ns_other = ns_other[0]
+
+    for folder in folders:
+        # exclude shared and other user's namespace
+        # TODO: list shared folders the user has write privileges ?
+        if folder.startswith(ns_other) or folder.startswith(ns_shared):
+            continue;
+
+        metadata = imap.get_metadata(folder)
+        log.debug(_("IMAP metadata for %r: %r") % (folder, metadata), level=9)
+        if metadata.has_key(folder) and ( \
+            metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith('event') \
+            or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith('event')):
+            calendars.append(folder)
+
+            # store default calendar folder in user record
+            if metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].endswith('.default'):
+                user_rec['_default_calendar'] = folder
+
+    # cache with user record
+    user_rec['_calendar_folders'] = calendars
+
+    return calendars
+
+
+def find_existing_event(itip_event, user_rec):
+    """
+        Search user's calendar folders for the given event (by UID)
+    """
+    global imap
+
+    event = None
+    for folder in list_user_calendars(user_rec):
+        log.debug(_("Searching folder %r for event %r") % (folder, itip_event['uid']), level=8)
+        imap.imap.m.select(imap.folder_utf7(folder))
+
+        typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (itip_event['uid']))
+        for num in reversed(data[0].split()):
+            typ, data = imap.imap.m.fetch(num, '(RFC822)')
+
+            event_message = message_from_string(data[0][1])
+
+            if event_message.is_multipart():
+                for part in event_message.walk():
+                    if part.get_content_type() == "application/calendar+xml":
+                        payload = part.get_payload(decode=True)
+                        event = event_from_string(payload)
+                        setattr(event, '_imap_folder', folder)
+                        break
+
+            if event and event.uid == itip_event['uid']:
+                return event
+
+    return event
+
+
+def check_availability(itip_event, receiving_user):
+    """
+        For the receiving user, determine if the event in question is in conflict.
+    """
+
+    start = time.time()
+    num_messages = 0
+    conflict = False
+
+    # return previously detected conflict
+    if itip_event.has_key('_conflicts'):
+        return not itip_event['_conflicts']
+
+    for folder in list_user_calendars(receiving_user):
+        log.debug(_("Listing events from folder %r") % (folder), level=8)
+        imap.imap.m.select(imap.folder_utf7(folder))
+
+        typ, data = imap.imap.m.search(None, '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")')
+        num_messages += len(data[0].split())
+
+        for num in reversed(data[0].split()):
+            event = None
+            typ, data = imap.imap.m.fetch(num, '(RFC822)')
+
+            event_message = message_from_string(data[0][1])
+
+            if event_message.is_multipart():
+                for part in event_message.walk():
+                    if part.get_content_type() == "application/calendar+xml":
+                        payload = part.get_payload(decode=True)
+                        event = event_from_string(payload)
+                        break
+
+                if event and event.uid:
+                    conflict = check_event_conflict(event, itip_event)
+                    if conflict:
+                        log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid']))
+                        break
+
+        if conflict:
+            break
+
+    end = time.time()
+    log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=9)
+
+    # remember the result of this check for further iterations
+    itip_event['_conflicts'] = conflict
+
+    return not conflict
+
+
+def store_event(event, user_rec, targetfolder=None):
+    """
+        Append the given event object to the user's default calendar
+    """
+
+    # find default calendar folder to save event to
+    if targetfolder is None:
+        targetfolder = list_user_calendars(user_rec)[0]
+        if user_rec.has_key('_default_calendar'):
+            targetfolder = user_rec['_default_calendar']
+
+    if not targetfolder:
+        log.error(_("Failed to save event: no calendar folder found for user %r") % (user_rec['mail']))
+        return Fasle
+
+    log.debug(_("Save event %r to user calendar %r") % (event.uid, targetfolder), level=8)
+
+    try:
+        imap.imap.m.select(imap.folder_utf7(targetfolder))
+        result = imap.imap.m.append(
+            imap.folder_utf7(targetfolder),
+            None,
+            None,
+            event.to_message().as_string()
+        )
+        return result
+
+    except Exception, e:
+        log.error(_("Failed to save event to user calendar at %r: %r") % (
+            targetfolder, e
+        ))
+
+    return False
+
+
+def delete_event(existing):
+    """
+        Removes the IMAP object with the given UID from a user's calendar folder
+    """
+    targetfolder = existing._imap_folder
+    imap.imap.m.select(imap.folder_utf7(targetfolder))
+
+    typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid)
+
+    log.debug(_("Delete event %r in %r: %r") % (
+        existing.uid, targetfolder, data
+    ), level=8)
+
+    for num in data[0].split():
+        imap.imap.m.store(num, '+FLAGS', '\\Deleted')
+
+    imap.imap.m.expunge()
+
+
+def invitation_response_text():
+    return _("""
+        %(name)s has %(status)s your invitation for %(summary)s.
+
+        *** This is an automated response sent by the Kolab Invitation system ***
+    """)


commit 223871e43e7ff6cd3c4dcd49e5c362a1fdf912df
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 22:09:42 2014 -0400

    Refactored some iTip functions into a dedicated module for shared use

diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
new file mode 100644
index 0000000..04b2d55
--- /dev/null
+++ b/pykolab/itip/__init__.py
@@ -0,0 +1,225 @@
+import icalendar
+import pykolab
+
+from pykolab.xml import to_dt
+from pykolab.xml import event_from_ical
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.wallace')
+
+
+def events_from_message(message, methods=None):
+    return objects_from_message(message, "VEVENT", methods)
+
+def todos_from_message(message, methods=None):
+    return objects_from_message(message, "VTODO", methods)
+
+
+def objects_from_message(message, objname, methods=None):
+    """
+        Obtain the iTip payload from email.message <message>
+    """
+    # Placeholder for any itip_objects found in the message.
+    itip_objects = []
+    seen_uids = []
+
+    # iTip methods we are actually interested in. Other methods will be ignored.
+    if methods is None:
+        methods = [ "REQUEST", "CANCEL" ]
+
+    # Are all iTip messages multipart? No! RFC 6047, section 2.4 states "A
+    # MIME body part containing content information that conforms to this
+    # document MUST have (...)" but does not state whether an iTip message must
+    # therefore also be multipart.
+
+    # Check each part
+    for part in message.walk():
+
+        # The iTip part MUST be Content-Type: text/calendar (RFC 6047, section 2.4)
+        # But in real word, other mime-types are used as well
+        if part.get_content_type() in [ "text/calendar", "text/x-vcalendar", "application/ics" ]:
+            if not str(part.get_param('method')).upper() in methods:
+                log.info(_("Method %r not really interesting for us.") % (part.get_param('method')))
+                continue
+
+            # Get the itip_payload
+            itip_payload = part.get_payload(decode=True)
+
+            log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9)
+
+            # Python iCalendar prior to 3.0 uses "from_string".
+            if hasattr(icalendar.Calendar, 'from_ical'):
+                cal = icalendar.Calendar.from_ical(itip_payload)
+            elif hasattr(icalendar.Calendar, 'from_string'):
+                cal = icalendar.Calendar.from_string(itip_payload)
+
+            # If we can't read it, we're out
+            else:
+                log.error(_("Could not read iTip from message."))
+                return []
+
+            for c in cal.walk():
+                if c.name == objname:
+                    itip = {}
+
+                    if c['uid'] in seen_uids:
+                        log.debug(_("Duplicate iTip object: %s") % (c['uid']), level=9)
+                        continue
+
+                    # From the event, take the following properties:
+                    #
+                    # - method
+                    # - uid
+                    # - sequence
+                    # - start
+                    # - end (if any)
+                    # - duration (if any)
+                    # - organizer
+                    # - attendees (if any)
+                    # - resources (if any)
+                    #
+
+                    itip['uid'] = str(c['uid'])
+                    itip['method'] = str(cal['method']).upper()
+                    itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
+
+                    if c.has_key('dtstart'):
+                        itip['start'] = c['dtstart'].dt
+                    else:
+                        log.error(_("iTip event without a start"))
+                        continue
+
+                    if c.has_key('dtend'):
+                        itip['end'] = c['dtend'].dt
+
+                    if c.has_key('duration'):
+                        itip['duration'] = c['duration'].dt
+                        itip['end'] = itip['start'] + c['duration'].dt
+
+                    itip['organizer'] = c['organizer']
+
+                    itip['attendees'] = c['attendee']
+
+                    if c.has_key('resources'):
+                        itip['resources'] = c['resources']
+
+                    itip['raw'] = itip_payload
+
+                    try:
+                        # TODO: distinguish event and todo here
+                        itip['xml'] = event_from_ical(c.to_ical())
+                    except Exception, e:
+                        log.error("event_from_ical() exception: %r" % (e))
+                        continue
+
+                    itip_objects.append(itip)
+
+                    seen_uids.append(c['uid'])
+
+                # end if c.name == "VEVENT"
+
+            # end for c in cal.walk()
+
+        # end if part.get_content_type() == "text/calendar"
+
+    # end for part in message.walk()
+
+    if not len(itip_objects) and not message.is_multipart():
+        log.debug(_("Message is not an iTip message (non-multipart message)"), level=5)
+
+    return itip_objects
+
+
+def check_event_conflict(kolab_event, itip_event):
+    """
+        Determine whether the given kolab event conflicts with the given itip event
+    """
+    conflict = False
+
+    # don't consider conflict with myself
+    if kolab_event.uid == itip_event['uid']:
+        return conflict
+
+    _es = to_dt(kolab_event.get_start())
+    _ee = to_dt(kolab_event.get_end())
+
+    # naive loops to check for collisions in (recurring) events
+    # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday)
+    while not conflict and _es is not None:
+        _is = to_dt(itip_event['start'])
+        _ie = to_dt(itip_event['end'])
+
+        while not conflict and _is is not None:
+            # log.debug("* Comparing event dates at %s/%s with %s/%s" % (_es, _ee, _is, _ie), level=9)
+            conflict = check_date_conflict(_es, _ee, _is, _ie)
+            _is = to_dt(itip_event['xml'].get_next_occurence(_is)) if kolab_event.is_recurring() else None
+            _ie = to_dt(itip_event['xml'].get_occurence_end_date(_is))
+
+        _es = to_dt(kolab_event.get_next_occurence(_es)) if kolab_event.is_recurring() else None
+        _ee = to_dt(kolab_event.get_occurence_end_date(_es))
+
+    return conflict
+
+
+def check_date_conflict(_es, _ee, _is, _ie):
+    """
+        Check the given event start/end dates for conflicts
+    """
+    conflict = False
+
+    # TODO: add margin for all-day dates (+13h; -12h)
+
+    if _es < _is:
+        if _es <= _ie:
+            if _ee <= _is:
+                conflict = False
+            else:
+                conflict = True
+        else:
+            conflict = True
+    elif _es == _is:
+        conflict = True
+    else: # _es > _is
+        if _es <= _ie:
+            conflict = True
+        else:
+            conflict = False
+    
+    return conflict
+
+
+def send_reply(from_address, itip_events, response_text, subject=None):
+    """
+        Send the given iCal events as a valid iTip REPLY to the organizer.
+    """
+
+    import smtplib
+    smtp = smtplib.SMTP("localhost", 10027)
+
+    conf = pykolab.getConf()
+
+    if conf.debuglevel > 8:
+        smtp.set_debuglevel(True)
+
+    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)
+
+        event_summary = itip_event['xml'].get_summary()
+        message_text = response_text % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
+
+        if subject is not None:
+            subject = subject % { 'summary':event_summary, 'status':_(participant_status), 'name':attendee.get_name() }
+
+        message = itip_event['xml'].to_message_itip(from_address,
+            method="REPLY",
+            participant_status=participant_status,
+            message_text=message_text,
+            subject=subject
+        )
+        smtp.sendmail(message['From'], message['To'], message.as_string())
+
+    smtp.quit()
diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py
index 62bfd27..bb586f8 100644
--- a/tests/unit/test-011-wallace_resources.py
+++ b/tests/unit/test-011-wallace_resources.py
@@ -302,29 +302,29 @@ class TestWallaceResources(unittest.TestCase):
         return None
 
     def test_001_itip_events_from_message(self):
-        itips1 = module_resources.itip_events_from_message(message_from_string(itip_multipart))
+        itips1 = pykolab.itip.events_from_message(message_from_string(itip_multipart))
         self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar")
         self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property")
 
-        itips2 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))
+        itips2 = pykolab.itip.events_from_message(message_from_string(itip_non_multipart))
         self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages")
 
-        itips3 = module_resources.itip_events_from_message(message_from_string(itip_application_ics))
+        itips3 = pykolab.itip.events_from_message(message_from_string(itip_application_ics))
         self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment")
 
-        itips4 = module_resources.itip_events_from_message(message_from_string(itip_google_multipart))
+        itips4 = pykolab.itip.events_from_message(message_from_string(itip_google_multipart))
         self.assertEqual(len(itips4), 1, "Multipart iTip message from Google")
 
-        itips5 = module_resources.itip_events_from_message(message_from_string(itip_empty))
+        itips5 = pykolab.itip.events_from_message(message_from_string(itip_empty))
         self.assertEqual(len(itips5), 0, "Simple plain text message")
 
         # invalid itip blocks
-        self.assertRaises(Exception, module_resources.itip_events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", "")))
+        self.assertRaises(Exception, pykolab.itip.events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", "")))
 
-        itips6 = module_resources.itip_events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;")))
+        itips6 = pykolab.itip.events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;")))
         self.assertEqual(len(itips6), 0, "Event with not DTSTART")
 
-        itips7 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH")))
+        itips7 = pykolab.itip.events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH")))
         self.assertEqual(len(itips7), 0, "Invalid METHOD")
 
 
@@ -337,7 +337,7 @@ class TestWallaceResources(unittest.TestCase):
 
     def test_003_resource_records_from_itip_events(self):
         message = message_from_string(itip_multipart)
-        itips = module_resources.itip_events_from_message(message)
+        itips = pykolab.itip.events_from_message(message)
 
         res = module_resources.resource_records_from_itip_events(itips)
         self.assertEqual(len(res), 2, "Return all attendee resources");
@@ -365,7 +365,7 @@ class TestWallaceResources(unittest.TestCase):
 
 
     def test_005_send_response_accept(self):
-        itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))
+        itip_event = pykolab.itip.events_from_message(message_from_string(itip_non_multipart))
         module_resources.send_response("resource-collection-car at example.org", itip_event)
 
         self.assertEqual(len(self.smtplog), 1);
@@ -384,7 +384,7 @@ class TestWallaceResources(unittest.TestCase):
 
     def test_006_send_response_delegate(self):
         # delegate resource-collection-car at example.org => resource-car-audi-a4 at example.org
-        itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))[0]
+        itip_event = pykolab.itip.events_from_message(message_from_string(itip_non_multipart))[0]
         itip_event['xml'].delegate('resource-collection-car at example.org', 'resource-car-audi-a4 at example.org')
         itip_event['xml'].set_attendee_participant_status(itip_event['xml'].get_attendee('resource-car-audi-a4 at example.org'), "ACCEPTED")
 
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 303252b..f398120 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -40,9 +40,10 @@ import kolabformat
 from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
-from pykolab.xml import event_from_ical
 from pykolab.xml import event_from_string
 from pykolab.xml import to_dt
+from pykolab.itip import events_from_message
+from pykolab.itip import check_event_conflict
 from pykolab.translate import _
 
 log = pykolab.getLogger('pykolab.wallace')
@@ -150,7 +151,7 @@ def execute(*args, **kw):
     # 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 = itip_events_from_message(message)
+        itip_events = events_from_message(message, ['REQUEST', 'CANCEL'])
     except Exception, e:
         log.error(_("Failed to parse iTip events from message: %r" % (e)))
         itip_events = []
@@ -473,33 +474,11 @@ def read_resource_calendar(resource_rec, itip_events):
                     event = pykolab.xml.event_from_string(payload)
 
                     for itip in itip_events:
-                        _es = to_dt(event.get_start())
-                        _ee = to_dt(event.get_end())
-
-                        conflict = False
-
-                        # naive loops to check for collisions in (recurring) events
-                        # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday)
-                        while not conflict and _es is not None:
-                            _is = to_dt(itip['start'])
-                            _ie = to_dt(itip['end'])
-
-                            while not conflict and _is is not None:
-                                log.debug("* Comparing event dates at %s/%s with %s/%s" % (_es, _ee, _is, _ie), level=9)
-                                conflict = check_date_conflict(_es, _ee, _is, _ie)
-                                _is = to_dt(itip['xml'].get_next_occurence(_is)) if event.is_recurring() else None
-                                _ie = to_dt(itip['xml'].get_occurence_end_date(_is))
-
-                            _es = to_dt(event.get_next_occurence(_es)) if event.is_recurring() else None
-                            _ee = to_dt(event.get_occurence_end_date(_es))
+                        conflict = check_event_conflict(event, itip)
 
                         if event.get_uid() == itip['uid']:
                             resource_rec['existing_events'].append(itip['uid'])
 
-                            # don't register conflict for updates
-                            if itip['sequence'] > 0 and itip['sequence'] >= event.get_sequence():
-                                conflict = False
-
                         if conflict:
                             log.info(
                                 _("Event %r conflicts with event %r") % (
@@ -513,29 +492,6 @@ def read_resource_calendar(resource_rec, itip_events):
 
     return num_messages
 
-def check_date_conflict(_es, _ee, _is, _ie):
-    conflict = False
-
-    # TODO: add margin for all-day dates (+13h; -12h)
-
-    if _es < _is:
-        if _es <= _ie:
-            if _ee <= _is:
-                conflict = False
-            else:
-                conflict = True
-        else:
-            conflict = True
-    elif _es == _is:
-        conflict = True
-    else: # _es > _is
-        if _es <= _ie:
-            conflict = True
-        else:
-            conflict = False
-    
-    return conflict
-
 
 def accept_reservation_request(itip_event, resource, delegator=None):
     """
@@ -617,118 +573,6 @@ def delete_resource_event(uid, resource):
     imap.imap.m.expunge()
 
 
-def itip_events_from_message(message):
-    """
-        Obtain the iTip payload from email.message <message>
-    """
-    # Placeholder for any itip_events found in the message.
-    itip_events = []
-    seen_uids = []
-
-    # iTip methods we are actually interested in. Other methods will be ignored.
-    itip_methods = [ "REQUEST", "CANCEL" ]
-
-    # Are all iTip messages multipart? No! RFC 6047, section 2.4 states "A
-    # MIME body part containing content information that conforms to this
-    # document MUST have (...)" but does not state whether an iTip message must
-    # therefore also be multipart.
-
-    # Check each part
-    for part in message.walk():
-
-        # The iTip part MUST be Content-Type: text/calendar (RFC 6047, section 2.4)
-        # But in real word, other mime-types are used as well
-        if part.get_content_type() in [ "text/calendar", "text/x-vcalendar", "application/ics" ]:
-            if not str(part.get_param('method')).upper() in itip_methods:
-                log.error(_("Method %r not really interesting for us.") % (part.get_param('method')))
-                continue
-
-            # Get the itip_payload
-            itip_payload = part.get_payload(decode=True)
-
-            log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9)
-
-            # Python iCalendar prior to 3.0 uses "from_string".
-            if hasattr(icalendar.Calendar, 'from_ical'):
-                cal = icalendar.Calendar.from_ical(itip_payload)
-            elif hasattr(icalendar.Calendar, 'from_string'):
-                cal = icalendar.Calendar.from_string(itip_payload)
-
-            # If we can't read it, we're out
-            else:
-                log.error(_("Could not read iTip from message."))
-                return []
-
-            for c in cal.walk():
-                if c.name == "VEVENT":
-                    itip = {}
-
-                    if c['uid'] in seen_uids:
-                        log.debug(_("Duplicate iTip event: %s") % (c['uid']), level=9)
-                        continue
-
-                    # From the event, take the following properties:
-                    #
-                    # - method
-                    # - uid
-                    # - sequence
-                    # - start
-                    # - end (if any)
-                    # - duration (if any)
-                    # - organizer
-                    # - attendees (if any)
-                    # - resources (if any)
-                    #
-
-                    itip['uid'] = str(c['uid'])
-                    itip['method'] = str(cal['method']).upper()
-                    itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
-
-                    if c.has_key('dtstart'):
-                        itip['start'] = c['dtstart'].dt
-                    else:
-                        log.error(_("iTip event without a start"))
-                        continue
-
-                    if c.has_key('dtend'):
-                        itip['end'] = c['dtend'].dt
-
-                    if c.has_key('duration'):
-                        itip['duration'] = c['duration'].dt
-                        itip['end'] = itip['start'] + c['duration'].dt
-
-                    itip['organizer'] = c['organizer']
-
-                    itip['attendees'] = c['attendee']
-
-                    if c.has_key('resources'):
-                        itip['resources'] = c['resources']
-
-                    itip['raw'] = itip_payload
-
-                    try:
-                        itip['xml'] = event_from_ical(c.to_ical())
-                    except Exception, e:
-                        log.error("event_from_ical() exception: %r" % (e))
-                        continue
-
-                    itip_events.append(itip)
-
-                    seen_uids.append(c['uid'])
-
-                # end if c.name == "VEVENT"
-
-            # end for c in cal.walk()
-
-        # end if part.get_content_type() == "text/calendar"
-
-    # end for part in message.walk()
-
-    if not len(itip_events) and not message.is_multipart():
-        log.debug(_("Message is not an iTip message (non-multipart message)"), level=5)
-
-    return itip_events
-
 def reject(filepath):
     new_filepath = os.path.join(
             mybasepath,
@@ -986,12 +830,6 @@ def send_response(from_address, itip_events, owner=None):
         resource, this will send an additional DELEGATED response message.
     """
 
-    import smtplib
-    smtp = smtplib.SMTP("localhost", 10027)
-
-    if conf.debuglevel > 8:
-        smtp.set_debuglevel(True)
-
     if isinstance(itip_events, dict):
         itip_events = [ itip_events ]
 
@@ -1000,6 +838,7 @@ def send_response(from_address, itip_events, owner=None):
         participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
 
         message_text = reservation_response_text(participant_status, owner)
+        subject_template = _("Reservation Request for %(summary)s was %(status)s")
 
         if participant_status == "DELEGATED":
             # Extra actions to take
@@ -1007,32 +846,21 @@ def send_response(from_address, itip_events, owner=None):
             delegatee = [a for a in itip_event['xml'].get_attendees() if from_address in [b.email() for b in a.get_delegated_from()]][0]
             delegatee_status = itip_event['xml'].get_ical_attendee_participant_status(delegatee)
 
-            message = itip_event['xml'].to_message_itip(delegatee.get_email(),
-                method="REPLY",
-                participant_status=delegatee_status,
-                message_text=reservation_response_text(delegatee_status, owner)
-            )
-            smtp.sendmail(message['From'], message['To'], message.as_string())
+            pykolab.itip.send_reply(delegatee.get_email(), itip_event, reservation_response_text(delegatee_status, owner),
+                subject=subject_template)
 
             # restore list of attendees after to_message_itip()
             itip_event['xml']._attendees = [ delegator, delegatee ]
             itip_event['xml'].event.setAttendees(itip_event['xml']._attendees)
 
-            participant_status = "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())
 
-        message = itip_event['xml'].to_message_itip(from_address,
-            method="REPLY",
-            participant_status=participant_status,
-            message_text=message_text
-        )
-        smtp.sendmail(message['From'], message['To'], message.as_string())
-
-    smtp.quit()
+        pykolab.itip.send_reply(from_address, itip_event, message_text,
+            subject=subject_template)
 
 
 def reservation_response_text(status, owner):


commit 4a76d06a534417920f76fae229c7130a12d2965f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 22:01:04 2014 -0400

    Load attendees list from libkolabxml container

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 9f0775b..ea84cc4 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -40,11 +40,16 @@ class Event(object):
                 self.event = kolabformat.Event()
             else:
                 self.event = kolabformat.readEvent(from_string, False)
+                self._load_attendees()
         else:
             self.from_ical(from_ical)
 
         self.uid = self.get_uid()
 
+    def _load_attendees(self):
+        for a in self.event.attendees():
+            self._attendees.append(Attendee(a.contact().email(), a.contact().name(), a.rsvp(), a.role(), a.partStat(), a.cutype()))
+
     def add_attendee(self, email, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL", params=None):
         attendee = Attendee(email, name, rsvp, role, participant_status, cutype, params)
         self._attendees.append(attendee)
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 61ea8ec..a44c4ec 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -11,6 +11,7 @@ from pykolab.xml import EventIntegrityError
 from pykolab.xml import InvalidAttendeeParticipantStatusError
 from pykolab.xml import InvalidEventDateError
 from pykolab.xml import event_from_ical
+from pykolab.xml import event_from_string
 
 class TestEventXML(unittest.TestCase):
     event = Event()
@@ -149,12 +150,14 @@ END:VCALENDAR
         self.event.set_summary("test")
         self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))
         self.event.set_end(datetime.datetime(2014, 05, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London")))
+        self.event.set_sequence(3)
 
         ical = icalendar.Calendar.from_ical(self.event.as_string_itip())
         event = ical.walk('VEVENT')[0]
 
         self.assertEqual(event['uid'], self.event.get_uid())
         self.assertEqual(event['summary'], "test")
+        self.assertEqual(event['sequence'], 3)
         self.assertIsInstance(event['dtstamp'].dt, datetime.datetime)
 
     def test_020_calendaring_recurrence(self):
@@ -214,6 +217,90 @@ END:VCALENDAR
         self.assertEqual(self.event.get_next_occurence(_start), None)
         self.assertEqual(self.event.get_last_occurrence(), None)
 
+    def test_022_load_from_xml(self):
+        xml = """
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+  <vcalendar>
+    <properties>
+      <prodid>
+        <text>Libkolabxml-1.1</text>
+      </prodid>
+      <version>
+        <text>2.0</text>
+      </version>
+      <x-kolab-version>
+        <text>3.1.0</text>
+      </x-kolab-version>
+    </properties>
+    <components>
+      <vevent>
+        <properties>
+          <uid>
+            <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text>
+          </uid>
+          <created>
+            <date-time>2014-07-07T01:28:23Z</date-time>
+          </created>
+          <dtstamp>
+            <date-time>2014-07-07T01:28:23Z</date-time>
+          </dtstamp>
+          <sequence>
+            <integer>1</integer>
+          </sequence>
+          <class>
+            <text>PUBLIC</text>
+          </class>
+          <dtstart>
+            <parameters>
+              <tzid>
+                <text>/kolab.org/Europe/London</text>
+              </tzid>
+            </parameters>
+            <date-time>2014-08-13T10:00:00</date-time>
+          </dtstart>
+          <dtend>
+            <parameters>
+              <tzid><text>/kolab.org/Europe/London</text></tzid>
+            </parameters>
+            <date-time>2014-08-13T14:00:00</date-time>
+          </dtend>
+          <summary>
+            <text>test</text>
+          </summary>
+          <organizer>
+            <parameters>
+              <cn><text>Doe, John</text></cn>
+            </parameters>
+            <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
+          </organizer>
+          <attendee>
+            <parameters>
+              <partstat><text>ACCEPTED</text></partstat>
+              <role><text>REQ-PARTICIPANT</text></role>
+              <rsvp><boolean>true</boolean></rsvp>
+            </parameters>
+            <cal-address>mailto:%3Cjane%40example.org%3E</cal-address>
+          </attendee>
+          <attendee>
+            <parameters>
+              <partstat><text>TENTATIVE</text></partstat>
+              <role><text>OPT-PARTICIPANT</text></role>
+            </parameters>
+            <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address>
+          </attendee>
+        </properties>
+      </vevent>
+    </components>
+  </vcalendar>
+</icalendar>
+"""
+        event = event_from_string(xml)
+        self.assertEqual(event.uid, '75c740bb-b3c6-442c-8021-ecbaeb0a025e')
+        self.assertEqual(event.get_attendee_by_email("jane at example.org").get_participant_status(), kolabformat.PartAccepted)
+        self.assertEqual(event.get_sequence(), 1)
+        self.assertIsInstance(event.get_start(), datetime.datetime)
+        self.assertEqual(str(event.get_start()), "2014-08-13 10:00:00+00:00")
+
 
 if __name__ == '__main__':
     unittest.main()


commit 3b83231c0da86de019938dec586d34f8c2c52467
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 17:14:15 2014 -0400

    Restore the original list of attendees after sending an iTip REPLY with only the replying attendee

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 280d2fb..9f0775b 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -725,6 +725,7 @@ class Event(object):
         msg = MIMEMultipart()
 
         msg_from = None
+        attendees = None
 
         if method == "REPLY":
             # TODO: Make user friendly name <email>
@@ -737,6 +738,7 @@ class Event(object):
                 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)
 
                     self._attendees = [attendee]
                     self.event.setAttendees(self._attendees)
@@ -779,7 +781,7 @@ class Event(object):
         msg['Date'] = formatdate(localtime=True)
 
         if subject is None:
-            subject = _("Reservation Request for %s was %s") % (self.get_summary(), _(participant_status))
+            subject = _("Invitation for %s was %s") % (self.get_summary(), _(participant_status))
 
         msg["Subject"] = subject
 
@@ -798,6 +800,12 @@ class Event(object):
 
         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):


commit 70e99bc6d51d625e2d1a784cf23cff627da84f5e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Sun Jul 6 17:12:57 2014 -0400

    Set (modified) contactreference back to Attendee object after changing CN

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index b496899..56699ce 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -151,6 +151,7 @@ class Attendee(kolabformat.Attendee):
 
     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():


commit d7ec7e24dfbe47f3dce2a6bb0b79cd3b42eaf92e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jul 4 22:19:25 2014 -0400

    Fix broken Auth.find_user() and LDAP._find_user_dn() methods; add wrapper method Auth.find_user_dn()

diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py
index fa081b8..7309b21 100644
--- a/pykolab/auth/__init__.py
+++ b/pykolab/auth/__init__.py
@@ -205,7 +205,10 @@ class Auth(pykolab.base.Base):
             return result
 
     def find_user(self, attr, value, **kw):
-        return self._auth._find_user(attr, value, **kw)
+        return self._auth.search_entry_by_attribute(attr, value, **kw)
+
+    def find_user_dn(self, login, kolabuser=False):
+        return self._auth._find_user_dn(login, kolabuser);
 
     def list_domains(self, domain=None):
         """
diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 148ecf8..d1a0b2d 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1987,16 +1987,17 @@ class LDAP(pykolab.base.Base):
                 else:
                     return _type
 
-    def _find_user_dn(self, login, realm):
+    def _find_user_dn(self, login, kolabuser=False):
         """
-            Find the distinguished name (DN) for an entry in LDAP.
+            Find the distinguished name (DN) for a (Kolab) user entry in LDAP.
         """
 
+        conf_prefix = 'kolab_' if kolabuser else ''
         domain_root_dn = self._kolab_domain_root_dn(self.domain)
 
-        base_dn = self.config_get('user_base_dn')
-        if base_dn == None:
-            base_dn = self.config_get('base_dn')
+        user_base_dn = self.config_get(conf_prefix + 'user_base_dn')
+        if user_base_dn == None:
+            user_base_dn = self.config_get('base_dn')
 
         auth_attrs = self.config_get_list('auth_attributes')
 
@@ -2004,18 +2005,21 @@ class LDAP(pykolab.base.Base):
 
         for auth_attr in auth_attrs:
             auth_search_filter.append('(%s=%s)' % (auth_attr,login))
-            auth_search_filter.append(
-                    '(%s=%s@%s)' % (
-                            auth_attr,
-                            login,
-                            self.domain
-                        )
-                )
+            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


commit 0ba02945a20bbf08755ecddbe41c52fbcd73dd93
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jul 4 15:51:47 2014 -0400

    Do not always set debug level=9 as (accidentally?) introduced in commit 4583e353

diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index b7ac1e9..eabc893 100644
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@ -312,8 +312,6 @@ def request_raw(method, api_uri, get=None, post=None, headers={}):
     if conf.debuglevel > 8:
         conn.set_debuglevel(9)
 
-    conn.set_debuglevel(9)
-
     if not get == None:
         _get = "?%s" % (urllib.urlencode(get))
     else:




More information about the commits mailing list