Branch 'dev/boddie-new/combined' - 54 commits - INSTALL conf/kolab.conf configure.ac kolabd/__init__.py po/POTFILES.in po/POTFILES.skip po/pykolab.pot pykolab/auth pykolab/conf pykolab/imap pykolab/itip pykolab/logger.py pykolab/setup pykolab/wap_client pykolab/xml saslauthd/__init__.py saslauthd/kolab-saslauthd.sysvinit share/templates tests/functional tests/unit wallace/module_invitationpolicy.py wallace/module_resources.py wallace/wallace.sysvinit

Paul Boddie boddie at kolabsys.com
Thu Sep 4 19:11:07 CEST 2014


 INSTALL                                                       |    2 
 conf/kolab.conf                                               |   10 
 configure.ac                                                  |    4 
 kolabd/__init__.py                                            |    6 
 po/POTFILES.in                                                |    5 
 po/POTFILES.skip                                              |    8 
 po/pykolab.pot                                                |  952 ++++++----
 pykolab/auth/ldap/__init__.py                                 |   23 
 pykolab/conf/__init__.py                                      |   10 
 pykolab/conf/defaults.py                                      |    3 
 pykolab/imap/__init__.py                                      |   13 
 pykolab/itip/__init__.py                                      |   28 
 pykolab/logger.py                                             |   34 
 pykolab/setup/setup_freebusy.py                               |   25 
 pykolab/setup/setup_mta.py                                    |   17 
 pykolab/setup/setup_mysql.py                                  |    4 
 pykolab/wap_client/__init__.py                                |   20 
 pykolab/xml/__init__.py                                       |   18 
 pykolab/xml/attendee.py                                       |    9 
 pykolab/xml/contact.py                                        |    2 
 pykolab/xml/event.py                                          |   50 
 pykolab/xml/recurrence_rule.py                                |   17 
 pykolab/xml/todo.py                                           |  207 ++
 pykolab/xml/utils.py                                          |  211 ++
 saslauthd/__init__.py                                         |   17 
 saslauthd/kolab-saslauthd.sysvinit                            |    4 
 share/templates/roundcubemail/acl.inc.php.tpl                 |    3 
 share/templates/roundcubemail/calendar.inc.php.tpl            |    4 
 share/templates/roundcubemail/config.inc.php.tpl              |    3 
 share/templates/roundcubemail/kolab_files.inc.php.tpl         |    2 
 share/templates/roundcubemail/kolab_folders.inc.php.tpl       |    8 
 share/templates/roundcubemail/password.inc.php.tpl            |    2 
 tests/functional/test_wallace/test_005_resource_invitation.py |    6 
 tests/functional/test_wallace/test_007_invitationpolicy.py    |  289 ++-
 tests/unit/test-003-event.py                                  |   58 
 tests/unit/test-012-wallace_invitationpolicy.py               |   32 
 tests/unit/test-016-todo.py                                   |  239 ++
 wallace/module_invitationpolicy.py                            |  520 +++--
 wallace/module_resources.py                                   |   29 
 wallace/wallace.sysvinit                                      |    2 
 40 files changed, 2257 insertions(+), 639 deletions(-)

New commits:
commit 6809a2ea710971b2ce9a98c125f31a01278939ad
Merge: 73f44a1 439b168
Author: Paul Boddie <paul at boddie.org.uk>
Date:   Thu Sep 4 19:10:35 2014 +0200

    Merge branch 'master' into dev/boddie-new/combined
    
    Conflicts:
    	pykolab/setup/setup_freebusy.py
    	pykolab/setup/setup_kolabd.py
    	pykolab/setup/setup_mta.py
    	pykolab/setup/setup_mysql.py
    	pykolab/setup/setup_roundcube.py
    	pykolab/setup/setup_syncroton.py

diff --cc conf/kolab.conf
index 627b23f,654c38e..28aa1b3
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@@ -206,8 -213,10 +213,11 @@@ kolab_group_filter = (|(objectclass=kol
  ; Same again
  sharedfolder_base_dn = ou=Shared Folders,%(base_dn)s
  sharedfolder_filter = (objectclass=kolabsharedfolder)
 +sharedfolder_delivery_address_attribute = mail
  
+ ; The attribute entry name that controls the ACLs set on a shared folder
+ sharedfolder_acl_entry_attribute = acl
+ 
  ; Same again. Resources live in a different OU structure or;
  ;
  ; - They would appear in the address book(s) as distribution lists or individual contacts,
diff --cc pykolab/setup/setup_freebusy.py
index 6993d5e,83baf6e..05309a3
--- a/pykolab/setup/setup_freebusy.py
+++ b/pykolab/setup/setup_freebusy.py
@@@ -88,11 -78,24 +88,24 @@@ def execute(*args, **kw)
      if scheme == None or scheme == "":
          scheme = 'imaps'
  
+     if scheme == "imaps" and port == 993:
+         scheme = "imap"
+         port = 143
+ 
      resources_imap_uri = '%s://%s:%s@%s:%s/%%kolabtargetfolder?acl=lrs' % (scheme, admin_login, admin_password, hostname, port)
-     users_imap_uri = '%s://%%mail:%s@%s:%s/?proxy_auth=%s' % (scheme, admin_password, hostname, port, admin_login)
+     users_imap_uri = '%s://%%s:%s@%s:%s/?proxy_auth=%s' % (scheme, admin_password, hostname, port, admin_login)
  
 -    freebusy_settings = {
 -            'directory "local"': {
 +    freebusy_settings = [
-             ('directory "kolab-users"', {
++            ('directory "local"': {
+                     'type': 'static',
+                     'fbsource': 'file:/var/lib/kolab-freebusy/%s.ifb',
 -                },
 -            'directory "local-cache"': {
++                }),
++            ('directory "local-cache"': {
+                     'type': 'static',
+                     'fbsource': 'file:/var/cache/kolab-freebusy/%s.ifb',
+                     'expires': '15m'
 -                },
 -            'directory "kolab-people"': {
++                }),
++            ('directory "kolab-people"': {
                      'type': 'ldap',
                      'host': conf.get('ldap', 'ldap_uri'),
                      'base_dn': conf.get('ldap', 'base_dn'),
@@@ -102,32 -105,30 +115,32 @@@
                      'attributes': 'mail',
                      'lc_attributes': 'mail',
                      'fbsource': users_imap_uri,
-                     'cacheto': '/var/cache/kolab-freebusy/%mail.ifb',
+                     'cacheto': '/var/cache/kolab-freebusy/%s.ifb',
                      'expires': '15m',
                      'loglevel': 300,
 -                },
 -            'directory "kolab-resources"': {
 +                }),
 +            ('directory "kolab-resources"', {
                      'type': 'ldap',
                      'host': conf.get('ldap', 'ldap_uri'),
                      'base_dn': conf.get('ldap', 'resource_base_dn'),
                      'bind_dn': conf.get('ldap', 'service_bind_dn'),
                      'bind_pw': conf.get('ldap', 'service_bind_pw'),
                      'attributes': 'mail, kolabtargetfolder',
-                     'filter': '(&(objectClass=kolabsharedfolder)(mail=%s))',
+                     'filter': '(&(objectClass=kolabsharedfolder)(kolabfoldertype=event)(mail=%s))',
                      'fbsource': resources_imap_uri,
-                     'cacheto': '/var/cache/kolab-freebusy/%mail.ifb',
+                     'cacheto': '/var/cache/kolab-freebusy/%s.ifb',
                      'expires': '15m',
 -                    'loglevel': 300,
 -                },
 -        }
 +                    'loglevel': 300
 +                }),
 +        ]
  
      cfg_parser = RawConfigParser()
 -    cfg_parser.read('/etc/kolab-freebusy/config.ini')
 +    cfg_parser.read(config)
 +
 +    will_update = False
  
 -    for section in freebusy_settings.keys():
 -        if len(freebusy_settings[section].keys()) < 1:
 +    for section, definitions in freebusy_settings:
 +        if not definitions:
              cfg_parser.remove_section(section)
              continue
  
diff --cc pykolab/setup/setup_mta.py
index c72b18a,88c6f6c..73c21a8
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@@ -113,296 -47,17 +113,301 @@@ def _execute(*args, **kw)
          group_filter = conf.get('ldap','group_filter')
  
      user_filter = conf.get('ldap','kolab_user_filter')
 -    if user_filter == None:
 +    if user_filter is None:
          user_filter = conf.get('ldap','user_filter')
  
 -    resource_filter = conf.get('ldap', 'resource_filter')
 +    server_host = utils.parse_ldap_uri(conf.get('ldap', 'ldap_uri'))[1]
  
 -    sharedfolder_filter = conf.get('ldap', 'sharedfolder_filter')
 +    definitions = {
 +            "base_dn":                  conf.get('ldap', 'base_dn'),
 +            "domain_base_dn":           conf.get('ldap', 'domain_base_dn'),
 +            "domain_filter":            conf.get('ldap', 'domain_filter').replace('*', '%s'),
 +            "domain_name_attribute":    conf.get('ldap', 'domain_name_attribute'),
 +            "group_base_dn":            conf.get('ldap', 'group_base_dn'),
 +            "server_host":              server_host,
 +            "service_bind_dn":          conf.get('ldap', 'service_bind_dn'),
 +            "service_bind_pw":          conf.get('ldap', 'service_bind_pw'),
 +            "kolab_user_filter":        user_filter,
 +            "kolab_group_filter":       group_filter,
 +            "resource_filter":          conf.get('ldap', 'resource_filter'),
 +            "sharedfolder_filter":      conf.get('ldap', 'sharedfolder_filter'),
 +        }
  
 -    server_host = utils.parse_ldap_uri(conf.get('ldap', 'ldap_uri'))[1]
 +    files = [
 +        ("local_recipient_maps.cf",               local_recipient_maps),
 +        ("mydestination.cf",                      mydestination),
 +        ("mailenabled_distgroups.cf",             mailenabled_distgroups),
 +        ("mailenabled_dynamic_distgroups.cf",     mailenabled_dynamic_distgroups),
 +        ("transport_maps.cf",                     transport_maps),
 +        ("virtual_alias_maps.cf",                 virtual_alias_maps),
 +        ("virtual_alias_maps_mailforwarding.cf",  virtual_alias_maps_mailforwarding),
 +        ("virtual_alias_maps_sharedfolders.cf",   virtual_alias_maps_sharedfolders),
 +        ]
 +
 +    if not isdir(prefix):
 +        mkdir(prefix, 0770)
 +
 +    for filename, data in files:
 +        pathname = join(prefix, filename)
 +        data = data % definitions
 +        if conf.check_only:
 +            matching_config = matching_config and file_contains_data(pathname, data, exact=True)
 +        else:
 +            fp = open(pathname, 'w')
 +            fp.write(data)
 +            fp.close()
 +
 +    # Check to see if the transport file was already written.
 +
 +    transport_file = "/etc/postfix/transport"
 +    transport_file_content = postfix_transport % {'domain': conf.get('kolab', 'primary_domain')}
 +
 +    matching_transport = file_contains_data(transport_file, transport_file_content)
 +    matching_config = matching_config and matching_transport
 +
 +    if matching_transport and not conf.reset_postfix_config and not conf.check_only:
 +        utils.message(_("Postfix transport file contains Kolab definition. Not updating the transport mapping."))
 +    elif not conf.check_only:
 +        fp = open(transport_file, 'a')
 +        fp.write(transport_file_content)
 +        fp.close()
 +
 +        call(["postmap", transport_file])
 +
 +    # Initialise certificate.
 +
 +    current_certificate = myaugeas.get(join(setting_base, 'smtpd_tls_cert_file'))
 +    current_key = myaugeas.get(join(setting_base, 'smtpd_tls_key_file'))
 +    unset_certificate = not current_certificate or not current_key
 +
 +    # Ask for details of the certificate, if appropriate.
 +
 +    if (unset_certificate or conf.reset_postfix_config) and not conf.check_only:
 +        default_certificate_name = is_debian() and 'ssl-cert-snakeoil' or 'localhost'
 +
 +        certificate_name = ask_question("kolab-conf/ssl-certificate-selection",
 +                _("""
 +                    Please indicate the name of the certificate to be used
 +                    by Postfix. If this is a new certificate, an attempt
 +                    will be made to create it. Note that this should be only
 +                    a simple name like "localhost" or "ssl-cert-snakeoil",
 +                    not a filename or pathname.
 +                    """),
 +                _("Certificate name"),
 +                default=default_certificate_name
 +            ).strip()
 +
 +        current_certificate = get_certificate_path(certificate_name)
 +        current_key = get_private_key_path(certificate_name)
 +
 +    # Detect and create a certificate, if appropriate.
 +    # Here, we test the certificate and not the key because there can be an
 +    # inconsistency between usage of .pem and .key for the keys, but
 +    # certificates always seem to use .pem as their suffix.
 +
 +    missing_certificate = not isfile(current_certificate)
 +
 +    if missing_certificate and not conf.check_only:
 +        make_ssl_certificate(current_key)
 +
 +    matching_config = matching_config and not unset_certificate and not missing_certificate
 +
 +    # Acquire the settings from a global defined below, duplicating the global
 +    # to be safe.
 +
 +    postfix_main_settings = {}
 +    postfix_main_settings.update(_postfix_main_settings)
 +
 +    if unset_certificate:
 +        postfix_main_settings['smtpd_tls_cert_file'] = current_certificate
 +        postfix_main_settings['smtpd_tls_key_file'] = current_key
 +
 +    # Copy header checks files.
 +
 +    for hc_file in ['inbound', 'internal', 'submission']:
 +        if not isfile("/etc/postfix/header_checks.%s" % hc_file):
 +            input_file = get_template_path('header_checks.%s' % hc_file)
 +
 +            missing_header_check_file = input_file is not None
 +            matching_config = matching_config and not missing_header_check_file
 +
 +            if missing_header_check_file and not conf.check_only:
 +                shutil.copy(input_file, "/etc/postfix/header_checks.%s" % hc_file)
 +                call(["postmap", "/etc/postfix/header_checks.%s" % hc_file])
 +
 +    # Update the main Postfix configuration.
 +
 +    for setting_key, proposed_value in postfix_main_settings.items():
 +        setting = join(setting_base, setting_key)
 +        current_value = myaugeas.get(setting)
 +
 +        # When only checking the configuration, exit the loop upon seeing any
 +        # difference.
 +
 +        if conf.check_only:
 +            matching_config = matching_config and current_value == proposed_value
 +            if not matching_config:
 +                break
 +            else:
 +                continue
 +
 +        # Handle absent regions of the configuration.
 +
 +        if current_value is None:
 +            try:
 +                myaugeas.set(setting, proposed_value)
 +            except:
 +                insert_paths = myaugeas.match('/files/etc/postfix/main.cf/*')
 +                myaugeas.insert(insert_paths[-1], setting_key, False)
 +
 +        log.debug(_("Setting key %r to %r") % (setting_key, proposed_value), level=8)
 +        myaugeas.set(setting, proposed_value)
 +
 +    if (not matching_config or conf.reset_postfix_config) and not conf.check_only:
 +        myaugeas.save()
 +
 +    # Update the master Postfix configuration.
 +
 +    postfix_master_settings = {}
 +
 +    if exists('/usr/lib/postfix/kolab_smtp_access_policy'):
 +        postfix_master_settings['kolab_sap_executable_path'] = '/usr/lib/postfix/kolab_smtp_access_policy'
 +    else:
 +        postfix_master_settings['kolab_sap_executable_path'] = '/usr/libexec/postfix/kolab_smtp_access_policy'
 +
 +    template_file = get_template_path('master.cf.tpl')
 +    output_file = '/etc/postfix/master.cf'
 +
 +    if template_file is not None:
 +        matching_config = instantiate_template(template_file, output_file, [postfix_master_settings], check_only=conf.check_only) and matching_config
 +    else:
 +        log.error(_("Could not write out Postfix configuration file %s") % output_file)
 +        return
 +
 +    # Configure Amavis.
 +
 +    amavisd_settings = {
 +            'ldap_server': server_host,
 +            'ldap_bind_dn': conf.get('ldap', 'service_bind_dn'),
 +            'ldap_bind_pw': conf.get('ldap', 'service_bind_pw'),
 +            'primary_domain': conf.get('kolab', 'primary_domain'),
 +            'ldap_filter': "(|(mail=%m)(alias=%m))",
 +            'ldap_base_dn': conf.get('ldap', 'base_dn'),
 +        }
 +
-     template_file = None
- 
 +    # On RPM installations, Amavis configuration is contained within a single file.
 +
++    template_file = get_template_path('amavisd.conf.tpl')
++    output_file = None
++
 +    if isfile("/etc/amavisd/amavisd.conf"):
-         template_file = get_template_path('amavisd.conf.tpl')
 +        output_file = '/etc/amavisd/amavisd.conf'
++    elif isfile("/etc/amavis/amavisd.conf"):
++        output_file = '/etc/amavis/amavisd.conf'
++    elif isfile("/etc/amavisd.conf"):
++        output_file = '/etc/amavisd.conf'
 +
++    if output_file is not None:
 +        if template_file is not None:
 +            matching_config = instantiate_template(template_file, output_file, [amavisd_settings], check_only=conf.check_only) and matching_config
 +        else:
 +            log.error(_("Could not write out Amavis configuration file %s") % output_file)
 +            return
 +
 +    # On APT installations, /etc/amavis/conf.d/ is a directory with many more files.
 +    #
 +    # Somebody could work on enhancement request #1080 to configure LDAP lookups,
 +    # while really it isn't required.
  
 -    files = {
 -            "/etc/postfix/ldap/local_recipient_maps.cf": """
 +    else:
 +        log.info(_("Not writing out any configuration for Amavis."))
 +
 +    # When only checking, give the status and return.
 +
 +    if conf.check_only:
 +        utils.setup_status("mta", matching_config and _("setup done") or _("needs setup"))
 +        return
 +
 +    # On debian wheezy amavisd-new expects '/etc/mailname' - possibly remediable through
 +    # the #1080 enhancement mentioned above, but here's a quick fix.
 +
 +    f = open('/etc/mailname','w')
 +    f.writelines(conf.get('kolab', 'primary_domain'))
 +    f.close()
 +
 +    if isdir('/etc/postfix/sasl/'):
 +        fp = open('/etc/postfix/sasl/smtpd.conf', 'w')
 +        fp.write("pwcheck_method: saslauthd\n")
 +        fp.write("mech_list: plain login\n")
 +        fp.close()
 +
 +    # Update the configuration file. By definition, any local mail transport
 +    # agent provides a local SMTP server.
 +
 +    if not conf.has_section('smtp'):
 +        conf.add_section('smtp')
 +    conf.command_set('smtp', 'host', 'localhost')
 +
 +    fp = open(conf.defaults.config_file, "w+")
 +    conf.cfg_parser.write(fp)
 +    fp.close()
 +
 +    # Configure and manipulate the services.
 +
 +    set_service_default('spamassassin', 'ENABLED', '1')
 +    set_service_default('wallace', 'START', 'yes')
 +
 +    amavis = is_debian() and 'amavis' or 'amavisd'
-     clamav = is_debian() and 'clamav-daemon' or 'clamd.amavisd'
++    clamav = is_debian() and 'clamav-daemon' or 'clamd at amavisd'
 +
 +    if not (
 +        control_service('postfix', 'restart') and
 +        control_service(amavis, 'restart') and
 +        control_service(clamav, 'restart') and
 +        control_service('wallace', 'restart')
 +        ):
 +
 +        log.error(_("Could not start the postfix, clamav and amavis services."))
 +
 +    if not (
 +        configure_service('postfix', True) and
 +        configure_service(amavis, True) and
 +        configure_service(clamav, True) and
 +        configure_service('wallace', True)
 +        ):
 +
 +        log.error(_("Could not configure to start on boot, the " + \
 +                "postfix, clamav and amavis services."))
 +
 +# Data used by the above code.
 +
 +_postfix_main_settings = {
 +        "inet_interfaces": "all",
 +        "recipient_delimiter": "+",
 +        "local_recipient_maps": "ldap:/etc/postfix/ldap/local_recipient_maps.cf",
 +        "mydestination": "ldap:/etc/postfix/ldap/mydestination.cf",
 +        "transport_maps": "ldap:/etc/postfix/ldap/transport_maps.cf, hash:/etc/postfix/transport",
 +        "virtual_alias_maps": "$alias_maps, ldap:/etc/postfix/ldap/virtual_alias_maps.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_mailforwarding.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_sharedfolders.cf, ldap:/etc/postfix/ldap/mailenabled_distgroups.cf, ldap:/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf",
 +        "smtpd_tls_auth_only": "yes",
 +        "smtpd_tls_security_level": "may",
 +        "smtp_tls_security_level": "may",
 +        "smtpd_sasl_auth_enable": "yes",
-         "smtpd_sender_login_maps": "$relay_recipient_maps",
++        "smtpd_sender_login_maps": "$local_recipient_maps",
 +        "smtpd_sender_restrictions": "permit_mynetworks, reject_sender_login_mismatch",
 +        "smtpd_recipient_restrictions": "permit_mynetworks, reject_unauth_pipelining, reject_rbl_client zen.spamhaus.org, reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/recipient_policy_incoming, permit",
 +        "smtpd_sender_restrictions": "permit_mynetworks, check_policy_service unix:private/sender_policy_incoming",
 +        "submission_recipient_restrictions": "check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject",
 +        "submission_sender_restrictions": "reject_non_fqdn_sender, check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject",
 +        "submission_data_restrictions": "check_policy_service unix:private/submission_policy",
 +        "content_filter": "smtp-amavis:[127.0.0.1]:10024"
 +    }
 +
 +postfix_transport = """
 +# Shared Folder Delivery for %(domain)s:
 +shared@%(domain)s\t\tlmtp:unix:/var/lib/imap/socket/lmtp
 +"""
 +
 +local_recipient_maps = """\
  server_host = %(server_host)s
  server_port = 389
  version = 3
@@@ -535,7 -224,243 +540,7 @@@ domain = ldap:/etc/postfix/ldap/mydesti
  bind_dn = %(service_bind_dn)s
  bind_pw = %(service_bind_pw)s
  
- query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabsharedfolder))
+ query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabsharedfolder)(kolabFolderType=mail))
  result_attribute = kolabtargetfolder
 -result_format = shared+%%s
 -""" % {
 -                        "base_dn": conf.get('ldap', 'base_dn'),
 -                        "server_host": server_host,
 -                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
 -                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
 -                    },
 -        }
 -
 -    if not os.path.isdir('/etc/postfix/ldap'):
 -        os.mkdir('/etc/postfix/ldap/', 0770)
 -
 -    for filename in files.keys():
 -        fp = open(filename, 'w')
 -        fp.write(files[filename])
 -        fp.close()
 -
 -    fp = open('/etc/postfix/transport', 'a')
 -    fp.write("\n# Shared Folder Delivery for %(domain)s:\nshared@%(domain)s\t\tlmtp:unix:/var/lib/imap/socket/lmtp\n" % {'domain': conf.get('kolab', 'primary_domain')})
 -    fp.close()
 -
 -    subprocess.call(["postmap", "/etc/postfix/transport"])
 -
 -    postfix_main_settings = {
 -            "inet_interfaces": "all",
 -            "recipient_delimiter": "+",
 -            "local_recipient_maps": "ldap:/etc/postfix/ldap/local_recipient_maps.cf",
 -            "mydestination": "ldap:/etc/postfix/ldap/mydestination.cf",
 -            "transport_maps": "ldap:/etc/postfix/ldap/transport_maps.cf, hash:/etc/postfix/transport",
 -            "virtual_alias_maps": "$alias_maps, ldap:/etc/postfix/ldap/virtual_alias_maps.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_mailforwarding.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_sharedfolders.cf, ldap:/etc/postfix/ldap/mailenabled_distgroups.cf, ldap:/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf",
 -            "smtpd_tls_auth_only": "yes",
 -            "smtpd_tls_security_level": "may",
 -            "smtp_tls_security_level": "may",
 -            "smtpd_sasl_auth_enable": "yes",
 -            "smtpd_sender_login_maps": "$local_recipient_maps",
 -            "smtpd_sender_restrictions": "permit_mynetworks, reject_sender_login_mismatch",
 -            "smtpd_recipient_restrictions": "permit_mynetworks, reject_unauth_pipelining, reject_rbl_client zen.spamhaus.org, reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/recipient_policy_incoming, permit",
 -            "smtpd_sender_restrictions": "permit_mynetworks, check_policy_service unix:private/sender_policy_incoming",
 -            "submission_recipient_restrictions": "check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject",
 -            "submission_sender_restrictions": "reject_non_fqdn_sender, check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject",
 -            "submission_data_restrictions": "check_policy_service unix:private/submission_policy",
 -            "content_filter": "smtp-amavis:[127.0.0.1]:10024"
 -
 -        }
 -
 -    if os.path.isfile('/etc/pki/tls/certs/make-dummy-cert') and not os.path.isfile('/etc/pki/tls/private/localhost.pem'):
 -        subprocess.call(['/etc/pki/tls/certs/make-dummy-cert', '/etc/pki/tls/private/localhost.pem'])
 -
 -    if os.path.isfile('/etc/pki/tls/private/localhost.pem'):
 -        postfix_main_settings['smtpd_tls_cert_file'] = "/etc/pki/tls/private/localhost.pem"
 -        postfix_main_settings['smtpd_tls_key_file'] = "/etc/pki/tls/private/localhost.pem"
 -
 -    if not os.path.isfile('/etc/postfix/main.cf'):
 -        if os.path.isfile('/usr/share/postfix/main.cf.debian'):
 -            shutil.copy(
 -                    '/usr/share/postfix/main.cf.debian',
 -                    '/etc/postfix/main.cf'
 -                )
 -
 -    # Copy header checks files
 -    for hc_file in [ 'inbound', 'internal', 'submission' ]:
 -        if not os.path.isfile("/etc/postfix/header_checks.%s" % (hc_file)):
 -            if os.path.isfile('/etc/kolab/templates/header_checks.%s' % (hc_file)):
 -                input_file = '/etc/kolab/templates/header_checks.%s' % (hc_file)
 -            elif os.path.isfile('/usr/share/kolab/templates/header_checks.%s' % (hc_file)):
 -                input_file = '/usr/share/kolab/templates/header_checks.%s' % (hc_file)
 -            elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'header_checks.%s' % (hc_file)))):
 -                input_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'header_checks.%s' % (hc_file)))
 -
 -            shutil.copy(input_file, "/etc/postfix/header_checks.%s" % (hc_file))
 -            subprocess.call(["postmap", "/etc/postfix/header_checks.%s" % (hc_file)])
 -
 -    myaugeas = Augeas()
 -
 -    setting_base = '/files/etc/postfix/main.cf/'
 -
 -    for setting_key in postfix_main_settings.keys():
 -        setting = os.path.join(setting_base,setting_key)
 -        current_value = myaugeas.get(setting)
 -
 -        if current_value == None:
 -            try:
 -                myaugeas.set(setting, postfix_main_settings[setting_key])
 -            except:
 -                insert_paths = myaugeas.match('/files/etc/postfix/main.cf/*')
 -                insert_path = insert_paths[(len(insert_paths)-1)]
 -                myaugeas.insert(insert_path, setting_key, False)
 -
 -        log.debug(_("Setting key %r to %r") % (setting_key, postfix_main_settings[setting_key]), level=8)
 -        myaugeas.set(setting, postfix_main_settings[setting_key])
 -
 -    myaugeas.save()
 -
 -    postfix_master_settings = {
 -        }
 -
 -    if os.path.exists('/usr/lib/postfix/kolab_smtp_access_policy'):
 -        postfix_master_settings['kolab_sap_executable_path'] = '/usr/lib/postfix/kolab_smtp_access_policy'
 -    else:
 -        postfix_master_settings['kolab_sap_executable_path'] = '/usr/libexec/postfix/kolab_smtp_access_policy'
 -
 -    template_file = None
 -
 -    if os.path.isfile('/etc/kolab/templates/master.cf.tpl'):
 -        template_file = '/etc/kolab/templates/master.cf.tpl'
 -    elif os.path.isfile('/usr/share/kolab/templates/master.cf.tpl'):
 -        template_file = '/usr/share/kolab/templates/master.cf.tpl'
 -    elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'master.cf.tpl'))):
 -        template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'master.cf.tpl'))
 -
 -    if not template_file == None:
 -        fp = open(template_file, 'r')
 -        template_definition = fp.read()
 -        fp.close()
 -
 -        t = Template(template_definition, searchList=[postfix_master_settings])
 -        fp = open('/etc/postfix/master.cf', 'w')
 -        fp.write(t.__str__())
 -        fp.close()
 -
 -    else:
 -        log.error(_("Could not write out Postfix configuration file /etc/postfix/master.cf"))
 -        return
 -
 -    if os.path.isdir('/etc/postfix/sasl/'):
 -        fp = open('/etc/postfix/sasl/smtpd.conf', 'w')
 -        fp.write("pwcheck_method: saslauthd\n")
 -        fp.write("mech_list: plain login\n")
 -        fp.close()
 -
 -    amavisd_settings = {
 -            'ldap_server': '%(server_host)s',
 -            'ldap_bind_dn': conf.get('ldap', 'service_bind_dn'),
 -            'ldap_bind_pw': conf.get('ldap', 'service_bind_pw'),
 -            'primary_domain': conf.get('kolab', 'primary_domain'),
 -            'ldap_filter': "(|(mail=%m)(alias=%m))",
 -            'ldap_base_dn': conf.get('ldap', 'base_dn'),
 -        }
 -
 -    template_file = None
 -
 -    # On RPM installations, Amavis configuration is contained within a single file.
 -    if os.path.isfile("/etc/amavisd/amavisd.conf"):
 -        if os.path.isfile('/etc/kolab/templates/amavisd.conf.tpl'):
 -            template_file = '/etc/kolab/templates/amavisd.conf.tpl'
 -        elif os.path.isfile('/usr/share/kolab/templates/amavisd.conf.tpl'):
 -            template_file = '/usr/share/kolab/templates/amavisd.conf.tpl'
 -        elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'amavisd.conf.tpl'))):
 -            template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'amavisd.conf.tpl'))
 -
 -        if not template_file == None:
 -            fp = open(template_file, 'r')
 -            template_definition = fp.read()
 -            fp.close()
 -
 -            t = Template(template_definition, searchList=[amavisd_settings])
 -
 -        fp = None
 -        if os.path.isdir('/etc/amavisd'):
 -            fp = open('/etc/amavisd/amavisd.conf', 'w')
 -        elif os.path.isdir('/etc/amavis'):
 -            fp = open('/etc/amavis/amavisd.conf', 'w')
 -        elif os.path.isfile('/etc/amavisd.conf'):
 -            fp = open('/etc/amavisd.conf', 'w')
 -
 -        if not fp == None:
 -            fp.write(t.__str__())
 -            fp.close()
 -
 -        else:
 -            log.error(_("Could not write out Amavis configuration file amavisd.conf"))
 -            return
 -
 -    # On APT installations, /etc/amavis/conf.d/ is a directory with many more files.
 -    #
 -    # Somebody could work on enhancement request #1080 to configure LDAP lookups,
 -    # while really it isn't required.
 -    else:
 -        log.info(_("Not writing out any configuration for Amavis."))
 -
 -    # On debian wheezy amavisd-new expects '/etc/mailname' - possibly remediable through
 -    # the #1080 enhancement mentioned above, but here's a quick fix.
 -    f = open('/etc/mailname','w')
 -    f.writelines(conf.get('kolab', 'primary_domain'))
 -    f.close()
 -
 -    if os.path.isfile('/etc/default/spamassassin'):
 -        myaugeas = Augeas()
 -        setting = os.path.join('/files/etc/default/spamassassin','ENABLED')
 -        if not myaugeas.get(setting) == '1':
 -            myaugeas.set(setting,'1')
 -            myaugeas.save()
 -        myaugeas.close()
 -
 -    if os.path.isfile('/etc/default/wallace'):
 -        myaugeas = Augeas()
 -        setting = os.path.join('/files/etc/default/wallace','START')
 -        if not myaugeas.get(setting) == 'yes':
 -            myaugeas.set(setting,'yes')
 -            myaugeas.save()
 -        myaugeas.close()
 -
 -    if os.path.isfile('/bin/systemctl'):
 -        subprocess.call(['systemctl', 'restart', 'postfix.service'])
 -        subprocess.call(['systemctl', 'restart', 'amavisd.service'])
 -        subprocess.call(['systemctl', 'restart', 'clamd at amavisd.service'])
 -        subprocess.call(['systemctl', 'restart', 'wallace.service'])
 -    elif os.path.isfile('/sbin/service'):
 -        subprocess.call(['service', 'postfix', 'restart'])
 -        subprocess.call(['service', 'amavisd', 'restart'])
 -        subprocess.call(['service', 'clamd.amavisd', 'restart'])
 -        subprocess.call(['service', 'wallace', 'restart'])
 -    elif os.path.isfile('/usr/sbin/service'):
 -        subprocess.call(['/usr/sbin/service','postfix','restart'])
 -        subprocess.call(['/usr/sbin/service','amavis','restart'])
 -        subprocess.call(['/usr/sbin/service','clamav-daemon','restart'])
 -        subprocess.call(['/usr/sbin/service','wallace','restart'])
 -    else:
 -        log.error(_("Could not start the postfix, clamav and amavisd services services."))
 -
 -    if os.path.isfile('/bin/systemctl'):
 -        subprocess.call(['systemctl', 'enable', 'postfix.service'])
 -        subprocess.call(['systemctl', 'enable', 'amavisd.service'])
 -        subprocess.call(['systemctl', 'enable', 'clamd at amavisd.service'])
 -        subprocess.call(['systemctl', 'enable', 'wallace.service'])
 -    elif os.path.isfile('/sbin/chkconfig'):
 -        subprocess.call(['chkconfig', 'postfix', 'on'])
 -        subprocess.call(['chkconfig', 'amavisd', 'on'])
 -        subprocess.call(['chkconfig', 'clamd.amavisd', 'on'])
 -        subprocess.call(['chkconfig', 'wallace', 'on'])
 -    elif os.path.isfile('/usr/sbin/update-rc.d'):
 -        subprocess.call(['/usr/sbin/update-rc.d', 'postfix', 'defaults'])
 -        subprocess.call(['/usr/sbin/update-rc.d', 'amavis', 'defaults'])
 -        subprocess.call(['/usr/sbin/update-rc.d', 'clamav-daemon', 'defaults'])
 -        subprocess.call(['/usr/sbin/update-rc.d', 'wallace', 'defaults'])
 -    else:
 -        log.error(_("Could not configure to start on boot, the " + \
 -                "postfix, clamav and amavisd services."))
 +result_format = <shared+%%s>
 +"""
diff --cc pykolab/setup/setup_mysql.py
index 377a3aa,fb1b102..241c4e8
--- a/pykolab/setup/setup_mysql.py
+++ b/pykolab/setup/setup_mysql.py
@@@ -109,85 -92,50 +109,86 @@@ def _execute(*args, **kw)
                          has completed, Kolab is going to discard and forget
                          about this password, but you will need it for
                          administrative tasks in MySQL.
 -                    """)
 -            )
 +                        """),
 +                    _("MySQL root password"),
 +                    default=utils.generate_password(),
 +                    password=True,
 +                    confirm=True
 +                )
 +
 +            p1 = Popen(['echo', "update mysql.user set Password=PASSWORD('%s') where User='root'" %
 +                mysql_root_password.replace("'", "''")], stdout=PIPE)
 +            p2 = Popen(['mysql'], stdin=p1.stdout)
 +            p2.communicate()
 +
 +            call(['mysql', '-e', 'flush privileges'])
 +
 +        # For an installation where the root password is still needed, it is
 +        # obtained here.
 +
 +        else:
 +            mysql_root_password = ask_question("kolab-conf/mysql-root-existing",
 +                    _("""
 +                        Please supply the root password for MySQL, so we can set
 +                        up user accounts for other components that use MySQL.
 +                        """),
 +                    _("MySQL root password"),
 +                    password=True
 +                )
  
 -        mysql_root_password = utils.ask_question(
 -                _("MySQL root password"),
 -                default=utils.generate_password(),
 -                password=True,
 -                confirm=True
 -            )
 +        # Write the defaults file in order to be able to connect in future.
  
 -        p1 = subprocess.Popen(['echo', 'UPDATE mysql.user SET Password=PASSWORD(\'%s\') WHERE User=\'root\';' % (mysql_root_password)], stdout=subprocess.PIPE)
 -        p2 = subprocess.Popen(['mysql'], stdin=p1.stdout)
 -        p1.stdout.close()
 -        p2.communicate()
 +        data = mysql_defaults % mysql_root_password
  
 -        p1 = subprocess.Popen(['echo', 'FLUSH PRIVILEGES;'], stdout=subprocess.PIPE)
 -        p2 = subprocess.Popen(['mysql'], stdin=p1.stdout)
 -        p1.stdout.close()
 -        p2.communicate()
 +        fp = open(defaults_file, 'w')
 +        os.chmod(defaults_file, 0600)
 +        fp.write(data)
 +        fp.close()
  
 -    data = """
 -[mysql]
 -user=root
 -password='%s'
 -""" % (mysql_root_password)
 -
 -    fp = open('/tmp/kolab-setup-my.cnf', 'w')
 -    os.chmod('/tmp/kolab-setup-my.cnf', 0600)
 -    fp.write(data)
 -    fp.close()
 -
 -    schema_file = None
 -    for root, directories, filenames in os.walk('/usr/share/doc/'):
 -        for filename in filenames:
 -            if filename.startswith('kolab_wap') and filename.endswith('.sql'):
 -                schema_file = os.path.join(root,filename)
 -
 -    if not schema_file == None:
 -        p1 = subprocess.Popen(['echo', 'create database kolab;'], stdout=subprocess.PIPE)
 -        p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf'], stdin=p1.stdout)
 -        p1.stdout.close()
 -        p2.communicate()
 -
 -        print >> sys.stderr, utils.multiline_message(
 +    # Find the schema file for the database.
 +
 +    schema_file = find_schema_file()
 +
 +    if schema_file is not None:
 +
 +        # Test for the database.
 +
 +        if not have_mysql_database(defaults_file, 'kolab'):
 +            if conf.check_only:
 +                utils.setup_status("mysql", _("needs setup"))
 +                return
 +
 +            call(['mysql', '--defaults-file=%s' % defaults_file, '-e', 'create database kolab'])
 +
 +            # Populate the schema.
 +
 +            p1 = Popen(['cat', schema_file], stdout=PIPE)
 +            p2 = Popen(['mysql', '--defaults-file=%s' % defaults_file, 'kolab'], stdin=p1.stdout)
 +            p2.communicate()
 +
 +        elif not conf.check_only:
 +            log.info(_("A database called %s already exists. Not creating another one.") % 'kolab')
 +
 +    else:
 +        log.error(_("Could not find the MySQL Kolab schema file"))
 +
 +    # Test for a MySQL user. Reset the user and configuration if explicitly
 +    # requested or if the configuration has not been modified.
 +
 +    wap_url_needs_setting = conf.get('kolab_wap', 'sql_uri') == 'mysql://user:pass@localhost/database'
++    access_policy_url_needs_setting = conf.get('kolab_smtp_access_policy', 'cache_uri') == 'mysql://user:pass@localhost/database'
 +
 +    if have_mysql_user(defaults_file, 'kolab') and not conf.reset_mysql_config and not wap_url_needs_setting:
 +
 +        if not conf.check_only:
 +            print >> sys.stderr, _("Kolab database account already exists and will not be configured.")
 +            log.info(_("A user called %s already exists. Not creating another one.") % 'kolab')
 +    else:
 +        if conf.check_only:
 +            utils.setup_status("mysql", _("needs setup"))
 +            return
 +
 +        mysql_kolab_password = ask_question("kolab-conf/mysql-kolab",
                  _("""
                          Please supply a password for the MySQL user 'kolab'.
                          This password will be used by Kolab services, such as
@@@ -199,32 -150,18 +200,35 @@@
                  confirm=True
              )
  
 -        p1 = subprocess.Popen(['echo', 'GRANT ALL PRIVILEGES ON kolab.* TO \'kolab\'@\'localhost\' IDENTIFIED BY \'%s\';' % (mysql_kolab_password)], stdout=subprocess.PIPE)
 -        p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf'], stdin=p1.stdout)
 -        p1.stdout.close()
 -        p2.communicate()
 +        if not have_mysql_user(defaults_file, 'kolab'):
 +            p1 = Popen(['echo', "grant all privileges on kolab.* to 'kolab'@'localhost' identified by '%s'" %
 +                mysql_kolab_password.replace("'", "''")], stdout=PIPE)
 +            p2 = Popen(['mysql', '--defaults-file=%s' % defaults_file], stdin=p1.stdout)
 +            p2.communicate()
  
 -        p1 = subprocess.Popen(['cat', schema_file], stdout=subprocess.PIPE)
 -        p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf', 'kolab'], stdin=p1.stdout)
 -        p1.stdout.close()
 -        p2.communicate()
 +        if wap_url_needs_setting or conf.reset_mysql_config:
 +            conf.command_set('kolab_wap', 'sql_uri', 'mysql://kolab:%s@localhost/kolab' % mysql_kolab_password)
  
 -        conf.command_set('kolab_wap', 'sql_uri', 'mysql://kolab:%s@localhost/kolab' % (mysql_kolab_password))
 -        conf.command_set('kolab_smtp_access_policy', 'cache_uri', 'mysql://kolab:%s@localhost/kolab' % (mysql_kolab_password))
 -    else:
 -        log.warning(_("Could not find the MySQL Kolab schema file"))
++        if access_policy_url_needs_setting or conf.reset_mysql_config:
++            conf.command_set('kolab_smtp_access_policy', 'cache_uri', 'mysql://kolab:%s@localhost/kolab' % mysql_kolab_password)
+ 
 +    # If nothing needed updating, assume that the setup was done.
 +
 +    if conf.check_only:
 +        utils.setup_status("mysql", _("setup done"))
 +
 +def find_schema_file():
 +    for webadmin_dir in glob('/usr/share/doc/kolab*'):
 +        for root, directories, filenames in os.walk(webadmin_dir):
 +            for filename in filenames:
 +                if filename.startswith('kolab_wap') and filename.endswith('.sql'):
 +                    return join(root,filename)
 +    return None
 +
 +# Data used by the code above.
 +
 +mysql_defaults = """\
 +[mysql]
 +user=root
 +password='%s'
 +"""
diff --cc pykolab/wap_client/__init__.py
index 7b2b565,2199e9f..68f2a7c
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@@ -74,10 -74,26 +74,26 @@@ def authenticate(username=None, passwor
          session_id = response['session_token']
          return True
  
- def connect():
-     global conn
+ def connect(uri=None):
+     global conn, API_SSL, API_PORT, API_HOSTNAME, API_BASE
+ 
+     if not uri == None:
+         result = urlparse(uri)
+ 
+         if hasattr(result, 'scheme') and result.scheme == 'https':
+             API_SSL = True
+             API_PORT = 443
+ 
+         if hasattr(result, 'hostname'):
+             API_HOSTNAME = result.hostname
+ 
+         if hasattr(result, 'port'):
+             API_PORT = result.port
+ 
+         if hasattr(result, 'path'):
+             API_BASE = result.path
  
 -    if conn == None:
 +    if conn is None:
          if API_SSL:
              conn = httplib.HTTPSConnection(API_HOSTNAME, API_PORT)
          else:


commit 439b16872c89843d85955d09e125c2355b69649a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 27 20:31:19 2014 +0200

    Don't use the mistakenly working /assets/ but use /roundcubemail/assets/

diff --git a/share/templates/roundcubemail/config.inc.php.tpl b/share/templates/roundcubemail/config.inc.php.tpl
index 61214f2..920423e 100644
--- a/share/templates/roundcubemail/config.inc.php.tpl
+++ b/share/templates/roundcubemail/config.inc.php.tpl
@@ -7,7 +7,7 @@
     \$config['des_key'] = "$des_key";
     \$config['username_domain'] = '$primary_domain';
     \$config['use_secure_urls'] = true;
-    \$config['assets_path'] = '/assets/';
+    \$config['assets_path'] = '/roundcubemail/assets/';
 
     \$config['mail_domain'] = '';
 


commit 0dedc8f52a0f8e3a98b9558559947946132098c7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Aug 22 16:30:38 2014 -0400

    Updated localization source files

diff --git a/po/POTFILES.in b/po/POTFILES.in
index 42d9803..8afa8b9 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -115,6 +115,7 @@ pykolab/xml/contact_reference.py
 pykolab/xml/event.py
 pykolab/xml/__init__.py
 pykolab/xml/recurrence_rule.py
+pykolab/xml/todo.py
 pykolab/xml/utils.py
 saslauthd/__init__.py
 saslauthd.py
@@ -171,6 +172,7 @@ 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
+tests/unit/test-016-todo.py
 test-wallace.py
 ucs/kolab_sieve.py
 ucs/listener.py
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index feb5583..91d4f18 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -40,6 +40,7 @@ pykolab/xml/._contact_reference.py
 pykolab/xml/._event.py
 pykolab/xml/.___init__.py
 pykolab/xml/._recurrence_rule.py
+pykolab/xml/._todo.py
 pykolab/xml/._utils.py
 tests/functional/._purge_users.py
 tests/functional/._resource_func.py
@@ -69,6 +70,7 @@ 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
+tests/unit/._test-016-todo.py
 wallace/.___init__.py
 wallace/._module_gpgencrypt.py
 wallace/._module_invitationpolicy.py
diff --git a/po/pykolab.pot b/po/pykolab.pot
index 6e4a662..d2152f1 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-08-07 11:26-0400\n"
+"POT-Creation-Date: 2014-08-22 16:29-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"
@@ -310,46 +310,46 @@ msgstr ""
 msgid "Path to the PID file to use."
 msgstr ""
 
-#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76
+#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:85
 #: ../wallace/__init__.py:135
 msgid "Run as user USERNAME"
 msgstr ""
 
-#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86
+#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:95
 #: ../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:329
+#: ../saslauthd/__init__.py:301 ../wallace/__init__.py:329
 #, python-format
 msgid "Group %s does not exist"
 msgstr ""
 
-#: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301
+#: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:310
 #: ../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:360
+#: ../saslauthd/__init__.py:332 ../wallace/__init__.py:360
 #, python-format
 msgid "User %s does not exist"
 msgstr ""
 
-#: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333
+#: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:342
 #: ../wallace/__init__.py:370
 #, python-format
 msgid "Switching real and effective user id to %d"
 msgstr ""
 
-#: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342
+#: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:351
 #: ../wallace/__init__.py:379
 msgid "Could not change real and effective uid and/or gid"
 msgstr ""
 
-#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133
+#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:142
 #: ../wallace/__init__.py:399
 msgid "Interrupted by user"
 msgstr ""
@@ -358,7 +358,7 @@ msgstr ""
 msgid "Traceback occurred, please report a "
 msgstr ""
 
-#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141
+#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:150
 #: ../wallace/__init__.py:408
 #, python-format
 msgid "Type Error: %s"
@@ -368,7 +368,7 @@ msgstr ""
 msgid "Could not connect to LDAP, is it running?"
 msgstr ""
 
-#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2166
+#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2171
 #: ../pykolab/cli/cmd_sync.py:36
 msgid "Listing domains..."
 msgstr ""
@@ -377,7 +377,7 @@ msgstr ""
 msgid "No domains. Not syncing"
 msgstr ""
 
-#: ../kolabd/__init__.py:275
+#: ../kolabd/__init__.py:279
 #, python-format
 msgid "added domains: %r, removed domains: %r"
 msgstr ""
@@ -668,95 +668,95 @@ msgstr ""
 msgid "Invalid DN, username and/or password."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1237 ../pykolab/auth/ldap/__init__.py:1254
-#: ../pykolab/auth/ldap/__init__.py:1616 ../pykolab/auth/ldap/__init__.py:1633
+#: ../pykolab/auth/ldap/__init__.py:1240 ../pykolab/auth/ldap/__init__.py:1257
+#: ../pykolab/auth/ldap/__init__.py:1621 ../pykolab/auth/ldap/__init__.py:1638
 #, python-format
 msgid "Found a subject %r with access %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1357
+#: ../pykolab/auth/ldap/__init__.py:1359
 #, python-format
 msgid "Entry %s attribute value: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1365
+#: ../pykolab/auth/ldap/__init__.py:1367
 #, python-format
 msgid "imap.user_mailbox_server(%r) result: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1685 ../pykolab/auth/ldap/__init__.py:1882
+#: ../pykolab/auth/ldap/__init__.py:1687 ../pykolab/auth/ldap/__init__.py:1887
 #, python-format
 msgid "Result from recipient policy: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1937
+#: ../pykolab/auth/ldap/__init__.py:1942
 #, python-format
 msgid "Kolab user %s does not have a result attribute %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2096
+#: ../pykolab/auth/ldap/__init__.py:2101
 #, python-format
 msgid "Finding domain root dn for domain %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2193
+#: ../pykolab/auth/ldap/__init__.py:2198
 msgid "Authentication database DOWN"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2277 ../pykolab/auth/ldap/__init__.py:2325
+#: ../pykolab/auth/ldap/__init__.py:2282 ../pykolab/auth/ldap/__init__.py:2330
 #, python-format
 msgid "Entry type: %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2414
+#: ../pykolab/auth/ldap/__init__.py:2419
 msgid "LDAP Search Result Data Entry:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2430
+#: ../pykolab/auth/ldap/__init__.py:2435
 msgid "Entry Change Notification attributes:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2435
+#: ../pykolab/auth/ldap/__init__.py:2440
 #, python-format
 msgid "Change Type: %r (%r)"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2443
+#: ../pykolab/auth/ldap/__init__.py:2448
 #, python-format
 msgid "Previous DN: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2498
+#: ../pykolab/auth/ldap/__init__.py:2503
 #, python-format
 msgid "Object %s searched no longer exists"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2508
+#: ../pykolab/auth/ldap/__init__.py:2513
 #, python-format
 msgid "%d results..."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2611
+#: ../pykolab/auth/ldap/__init__.py:2616
 #, python-format
 msgid "Searching with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2663
+#: ../pykolab/auth/ldap/__init__.py:2668
 #, python-format
 msgid "Checking for support for %s on %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2682
+#: ../pykolab/auth/ldap/__init__.py:2687
 #, python-format
 msgid "Found support for %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2727
+#: ../pykolab/auth/ldap/__init__.py:2732
 #, python-format
 msgid "An error occured using %s: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2733
+#: ../pykolab/auth/ldap/__init__.py:2738
 #, python-format
 msgid "%s"
 msgstr ""
@@ -1489,27 +1489,28 @@ msgid "This program has 9 levels of verbosity. Using the maximum of 9."
 msgstr ""
 
 #: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591
+#: ../pykolab/conf/__init__.py:595 ../pykolab/conf/__init__.py:601
 msgid "Cannot start SASL authentication daemon"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:602
+#: ../pykolab/conf/__init__.py:612
 msgid "No imaplib library found."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:612
+#: ../pykolab/conf/__init__.py:622
 msgid "No LMTP class found in the smtplib library."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:622
+#: ../pykolab/conf/__init__.py:632
 msgid "No SMTP class found in the smtplib library."
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:636
+#: ../pykolab/conf/__init__.py:646
 #, python-format
 msgid "Found you specified a specific set of items to test: %s"
 msgstr ""
 
-#: ../pykolab/conf/__init__.py:644
+#: ../pykolab/conf/__init__.py:654
 #, python-format
 msgid "Selectively selecting: %s"
 msgstr ""
@@ -1699,174 +1700,179 @@ msgstr ""
 msgid "%r has no attribute %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:396 ../pykolab/imap/__init__.py:431
+#: ../pykolab/imap/__init__.py:373
+#, python-format
+msgid "Could not set ACL for %s on folder %s: %r"
+msgstr ""
+
+#: ../pykolab/imap/__init__.py:407 ../pykolab/imap/__init__.py:442
 #, python-format
 msgid "Creating new shared folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:456 ../pykolab/imap/__init__.py:678
+#: ../pykolab/imap/__init__.py:467 ../pykolab/imap/__init__.py:689
 #, python-format
 msgid "Downcasing mailbox name %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:460
+#: ../pykolab/imap/__init__.py:471
 #, python-format
 msgid "Creating new mailbox for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:473
+#: ../pykolab/imap/__init__.py:484
 msgid "Waiting for the Cyrus IMAP Murder to settle..."
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:519
+#: ../pykolab/imap/__init__.py:530
 #, python-format
 msgid "Creating additional folders for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:538
+#: ../pykolab/imap/__init__.py:549
 #, python-format
 msgid "Waiting for the Cyrus murder to settle... %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:550
+#: ../pykolab/imap/__init__.py:561
 #, python-format
 msgid "Correcting additional folder name from %r to %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:556
+#: ../pykolab/imap/__init__.py:567
 #, python-format
 msgid "Mailbox already exists: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:596
+#: ../pykolab/imap/__init__.py:607
 msgid "Subscribing user to the additional folders"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:610
+#: ../pykolab/imap/__init__.py:621
 msgid "Using the following tests for folder subscriptions:"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:612
+#: ../pykolab/imap/__init__.py:623
 #, python-format
 msgid "    %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:615
+#: ../pykolab/imap/__init__.py:626
 #, python-format
 msgid "Folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:627
+#: ../pykolab/imap/__init__.py:638
 #, python-format
 msgid "Subscribing %s to folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:631
+#: ../pykolab/imap/__init__.py:642
 #, python-format
 msgid "Subscribing %s to folder %s failed: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:661
+#: ../pykolab/imap/__init__.py:672
 #, python-format
 msgid "Could not rename %s to reside on partition %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:694
+#: ../pykolab/imap/__init__.py:705
 #, python-format
 msgid "INBOX folder to rename (%s) does not exist"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:697 ../pykolab/imap/__init__.py:773
+#: ../pykolab/imap/__init__.py:708 ../pykolab/imap/__init__.py:784
 #, python-format
 msgid "Renaming INBOX from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:701
+#: ../pykolab/imap/__init__.py:712
 #, python-format
 msgid "Could not rename INBOX folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:703 ../pykolab/imap/__init__.py:777
+#: ../pykolab/imap/__init__.py:714 ../pykolab/imap/__init__.py:788
 #, python-format
 msgid "Moving INBOX folder %s won't succeed as target folder %s already exists"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:707
+#: ../pykolab/imap/__init__.py:718
 #, python-format
 msgid "Server for mailbox %r is %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:715
+#: ../pykolab/imap/__init__.py:726
 #, python-format
 msgid "Looking for folder '%s', we found folders: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:738
+#: ../pykolab/imap/__init__.py:749
 #, python-format
 msgid "Setting ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:749
+#: ../pykolab/imap/__init__.py:760
 #, python-format
 msgid "Removing ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:770
+#: ../pykolab/imap/__init__.py:781
 #, python-format
 msgid "Found old INBOX folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:779
+#: ../pykolab/imap/__init__.py:790
 #, python-format
 msgid "Did not find old folder user/%s to rename"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:781
+#: ../pykolab/imap/__init__.py:792
 msgid "Value for user is not a dictionary"
 msgstr ""
 
 #. TODO: Go in fact correct the quota.
-#: ../pykolab/imap/__init__.py:849
+#: ../pykolab/imap/__init__.py:860
 #, python-format
 msgid "Cannot get current IMAP quota for folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:862
+#: ../pykolab/imap/__init__.py:873
 #, python-format
 msgid "Quota for %s currently is %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:868
+#: ../pykolab/imap/__init__.py:879
 #, python-format
 msgid "Adjusting authentication database quota for folder %s to %d"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:873
+#: ../pykolab/imap/__init__.py:884
 #, python-format
 msgid "Correcting quota for %s to %s (currently %s)"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:950
+#: ../pykolab/imap/__init__.py:961
 #, python-format
 msgid "Checking folder: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:955
+#: ../pykolab/imap/__init__.py:966
 #, python-format
 msgid "Folder has no corresponding user (1): %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:958
+#: ../pykolab/imap/__init__.py:969
 #, python-format
 msgid "Folder has no corresponding user (2): %s"
 msgstr ""
 
 #. We got user identifier only
-#: ../pykolab/imap/__init__.py:973
+#: ../pykolab/imap/__init__.py:984
 msgid "Please don't give us just a user identifier"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:976
+#: ../pykolab/imap/__init__.py:987
 #, python-format
 msgid "Deleting folder %s"
 msgstr ""
@@ -1875,56 +1881,62 @@ msgstr ""
 msgid "Returning thread local configuration"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:43
+#: ../pykolab/itip/__init__.py:45
 #, python-format
 msgid "Method %r not really interesting for us."
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:49
+#: ../pykolab/itip/__init__.py:51
 #, python-format
 msgid "Raw iTip payload: %s"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:59
+#: ../pykolab/itip/__init__.py:61
 msgid "Could not read iTip from message."
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:67
+#: ../pykolab/itip/__init__.py:69
 #, python-format
 msgid "Duplicate iTip object: %s"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:90
+#: ../pykolab/itip/__init__.py:93
 msgid "iTip event without a start"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:132
+#: ../pykolab/itip/__init__.py:138
 msgid "Message is not an iTip message (non-multipart message)"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:229
+#: ../pykolab/itip/__init__.py:221
 #, python-format
-msgid "Failed to compose iTip reply message: %r"
+msgid "Send iTip reply %s for %s %r"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:240 ../pykolab/itip/__init__.py:284
-#: ../wallace/module_invitationpolicy.py:966
-#: ../wallace/module_resources.py:1131
+#: ../pykolab/itip/__init__.py:237
+#, python-format
+msgid "Failed to compose iTip reply message: %r: %s"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:248 ../pykolab/itip/__init__.py:292
+#: ../wallace/module_invitationpolicy.py:1063
+#: ../wallace/module_invitationpolicy.py:1121
+#: ../wallace/module_resources.py:1144
 #, python-format
 msgid "SMTP sendmail error: %r"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:272
+#: ../pykolab/itip/__init__.py:280
 #, python-format
 msgid "Failed to compose iTip request message: %r"
 msgstr ""
 
-#: ../pykolab/logger.py:173 ../pykolab/logger.py:179
+#: ../pykolab/logger.py:174 ../pykolab/logger.py:181
 #, python-format
 msgid "Could not change permissions on %s: %r"
 msgstr ""
 
-#: ../pykolab/logger.py:196
+#: ../pykolab/logger.py:198
 #, python-format
 msgid "Cannot log to file %s: %s"
 msgstr ""
@@ -2059,7 +2071,7 @@ msgid "user_delete: %r"
 msgstr ""
 
 #: ../pykolab/plugins/roundcubedb/__init__.py:55
-#: ../pykolab/setup/setup_roundcube.py:160
+#: ../pykolab/setup/setup_roundcube.py:161
 msgid "Roundcube installation path not found."
 msgstr ""
 
@@ -2103,18 +2115,18 @@ msgstr ""
 msgid "Could not start the cyrus-imapd and kolab-saslauthd services."
 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:237
-#: ../pykolab/setup/setup_syncroton.py:102
+#: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:90
+#: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:465
+#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:238
+#: ../pykolab/setup/setup_syncroton.py:105
 msgid "Could not configure to start on boot, the "
 msgstr ""
 
-#: ../pykolab/setup/setup_kolabd.py:43
+#: ../pykolab/setup/setup_kolabd.py:44
 msgid "Setup the Kolab daemon."
 msgstr ""
 
-#: ../pykolab/setup/setup_kolabd.py:51
+#: ../pykolab/setup/setup_kolabd.py:52
 #, python-format
 msgid ""
 "\n"
@@ -2124,7 +2136,7 @@ msgid ""
 "                        "
 msgstr ""
 
-#: ../pykolab/setup/setup_kolabd.py:72
+#: ../pykolab/setup/setup_kolabd.py:81
 msgid "Could not start the kolab server service."
 msgstr ""
 
@@ -2423,15 +2435,15 @@ msgstr ""
 msgid "Could not write out Postfix configuration file /etc/postfix/master.cf"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:397
+#: ../pykolab/setup/setup_mta.py:399
 msgid "Could not write out Amavis configuration file amavisd.conf"
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:405
+#: ../pykolab/setup/setup_mta.py:407
 msgid "Not writing out any configuration for Amavis."
 msgstr ""
 
-#: ../pykolab/setup/setup_mta.py:437
+#: ../pykolab/setup/setup_mta.py:447
 msgid "Could not start the postfix, clamav and amavisd services services."
 msgstr ""
 
@@ -2458,8 +2470,8 @@ msgid ""
 msgstr ""
 
 #: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99
-#: ../pykolab/setup/setup_roundcube.py:183
-#: ../pykolab/setup/setup_syncroton.py:63
+#: ../pykolab/setup/setup_roundcube.py:184
+#: ../pykolab/setup/setup_syncroton.py:66
 msgid "MySQL root password"
 msgstr ""
 
@@ -2493,7 +2505,7 @@ msgstr ""
 msgid "MySQL kolab password"
 msgstr ""
 
-#: ../pykolab/setup/setup_mysql.py:165
+#: ../pykolab/setup/setup_mysql.py:166
 msgid "Could not find the MySQL Kolab schema file"
 msgstr ""
 
@@ -2564,8 +2576,8 @@ msgstr ""
 msgid "Successfully compiled template %r, writing out to %r"
 msgstr ""
 
-#: ../pykolab/setup/setup_roundcube.py:228
-#: ../pykolab/setup/setup_syncroton.py:93
+#: ../pykolab/setup/setup_roundcube.py:229
+#: ../pykolab/setup/setup_syncroton.py:96
 msgid "Could not start the webserver server service."
 msgstr ""
 
@@ -2661,18 +2673,18 @@ msgstr ""
 msgid "Could not translate %s using locale %s"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:380
+#: ../pykolab/wap_client/__init__.py:396
 #, python-format
 msgid "Requesting %r with params %r"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:388
+#: ../pykolab/wap_client/__init__.py:404
 #, python-format
 msgid "Got response: %r"
 msgstr ""
 
 #. Some data is not JSON
-#: ../pykolab/wap_client/__init__.py:394
+#: ../pykolab/wap_client/__init__.py:410
 msgid "Response data is not JSON"
 msgstr ""
 
@@ -2697,134 +2709,304 @@ msgstr ""
 msgid "Delegated"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:14
+#: ../pykolab/xml/attendee.py:14 ../pykolab/xml/attendee.py:22
 msgid "Completed"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:15
-msgid "In Process"
+#: ../pykolab/xml/attendee.py:15 ../pykolab/xml/attendee.py:23
+msgid "Started"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:131 ../pykolab/xml/attendee.py:153
+#: ../pykolab/xml/attendee.py:132 ../pykolab/xml/attendee.py:154
 msgid "Not a valid attendee"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:138
+#: ../pykolab/xml/attendee.py:139
 msgid "No valid delegator references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:158
+#: ../pykolab/xml/attendee.py:159
 msgid "No valid delegatee references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:218
+#: ../pykolab/xml/attendee.py:219
 #, python-format
 msgid "Invalid cutype %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:230
+#: ../pykolab/xml/attendee.py:231
 #, python-format
 msgid "Invalid participant status %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:238
+#: ../pykolab/xml/attendee.py:239
 #, python-format
 msgid "Invalid role %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:146 ../pykolab/xml/event.py:780
-#: ../pykolab/xml/event.py:823
+#: ../pykolab/xml/event.py:149 ../pykolab/xml/event.py:784
+#: ../pykolab/xml/event.py:827
 msgid "Event start needs datetime.date or datetime.datetime instance"
 msgstr ""
 
-#: ../pykolab/xml/event.py:291
+#: ../pykolab/xml/event.py:294
 #, python-format
 msgid "No attendee with email or name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:299
+#: ../pykolab/xml/event.py:302
 #, python-format
 msgid "Invalid argument value attendee %r, must be basestring or Attendee"
 msgstr ""
 
-#: ../pykolab/xml/event.py:311
+#: ../pykolab/xml/event.py:314
 #, python-format
 msgid "No attendee with email %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:317
+#: ../pykolab/xml/event.py:320
 #, python-format
 msgid "No attendee with name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:488
+#: ../pykolab/xml/event.py:370 ../pykolab/xml/utils.py:151
+msgid "%Y-%m-%d"
+msgstr ""
+
+#: ../pykolab/xml/event.py:372 ../pykolab/xml/utils.py:152
+msgid "%H:%M (%Z)"
+msgstr ""
+
+#: ../pykolab/xml/event.py:496
 msgid "Invalid participant status"
 msgstr ""
 
-#: ../pykolab/xml/event.py:610
+#: ../pykolab/xml/event.py:618
 #, python-format
 msgid "Invalid classification %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:641
+#: ../pykolab/xml/event.py:649
 msgid "Event end needs datetime.date or datetime.datetime instance"
 msgstr ""
 
-#: ../pykolab/xml/event.py:651
+#: ../pykolab/xml/event.py:659
 #, python-format
 msgid "Invalid custom property name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:833
+#: ../pykolab/xml/event.py:837
 #, python-format
 msgid "Invalid status set: %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:1070
+#: ../pykolab/xml/event.py:1074
 msgid "No sender specified"
 msgstr ""
 
-#: ../pykolab/xml/event.py:1079
+#: ../pykolab/xml/event.py:1083
 #, python-format
 msgid "Invitation for %s was %s"
 msgstr ""
 
-#: ../pykolab/xml/event.py:1084
+#: ../pykolab/xml/event.py:1088
 msgid "This is an automated response to one of your event requests."
 msgstr ""
 
-#: ../saslauthd/__init__.py:99
+#: ../pykolab/xml/recurrence_rule.py:38
+#, python-format
+msgid "Every %d year(s)"
+msgstr ""
+
+#: ../pykolab/xml/recurrence_rule.py:39
+#, python-format
+msgid "Every %d month(s)"
+msgstr ""
+
+#: ../pykolab/xml/recurrence_rule.py:40
+#, python-format
+msgid "Every %d week(s)"
+msgstr ""
+
+#: ../pykolab/xml/recurrence_rule.py:41
+#, python-format
+msgid "Every %d day(s)"
+msgstr ""
+
+#: ../pykolab/xml/recurrence_rule.py:42
+#, python-format
+msgid "Every %d hours"
+msgstr ""
+
+#: ../pykolab/xml/recurrence_rule.py:43
+#, python-format
+msgid "Every %d minutes"
+msgstr ""
+
+#: ../pykolab/xml/recurrence_rule.py:44
+#, python-format
+msgid "Every %d seconds"
+msgstr ""
+
+#: ../pykolab/xml/todo.py:110
+msgid "Todo due needs datetime.date or datetime.datetime instance"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:120
+msgid "Name"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:121
+msgid "Summary"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:122
+msgid "Location"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:123
+msgid "Description"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:124
+msgid "URL"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:125
+msgid "Status"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:126
+msgid "Priority"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:127
+msgid "Attendee"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:128
+msgid "Start"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:129
+msgid "End"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:130
+msgid "Due"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:131
+msgid "Repeat"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:132
+msgid "Repeat Exception"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:133
+msgid "Organizer"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:134
+msgid "Attachment"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:135
+msgid "Alarm"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:136
+msgid "Classification"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:137
+msgid "Progress"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:182
+#, python-format
+msgid "for %d times"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:184
+#, python-format
+msgid "until %s"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:189
+msgid "Display message"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:190
+msgid "Send email"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:191
+msgid "Play sound"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:197
+#, python-format
+msgid "%s after"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:197
+#, python-format
+msgid "%s before"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:206
+#, python-format
+msgid "%d day(s)"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:212
+#, python-format
+msgid "%d hour(s)"
+msgstr ""
+
+#: ../pykolab/xml/utils.py:214
+#, python-format
+msgid "%d minute(s)"
+msgstr ""
+
+#: ../saslauthd/__init__.py:76
+msgid "Socket file to bind to."
+msgstr ""
+
+#: ../saslauthd/__init__.py:108
 #, python-format
 msgid "Could not create %r: %r"
 msgstr ""
 
-#: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145
+#: ../saslauthd/__init__.py:146 ../saslauthd/__init__.py:154
 #: ../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
+#: ../saslauthd/__init__.py:194
 msgid "kolab-saslauthd could not accept "
 msgstr ""
 
-#: ../saslauthd/__init__.py:190
+#: ../saslauthd/__init__.py:199
 msgid "Maximum tries exceeded, exiting"
 msgstr ""
 
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:192
-#: ../wallace/module_resources.py:1041
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:195
+#: ../wallace/module_resources.py:1054
 #, 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:615
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:631
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:662
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:700
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:756
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:769
-#: ../wallace/module_resources.py:1121
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:619
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:635
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:666
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:704
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:760
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:773
+#: ../wallace/module_resources.py:1134
 #, python-format
 msgid "Booking for %s has been %s"
 msgstr ""
@@ -2833,18 +3015,18 @@ msgstr ""
 #. check first confirmation message sent to resource owner (jane)
 #. check second confirmation message sent to resource owner (jane)
 #. check confirmation message sent to resource owner (jane)
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:652
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:690
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:728
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:745
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:799
-#: ../wallace/module_resources.py:1217
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:656
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:694
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:732
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:749
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:803
+#: ../wallace/module_resources.py:1230
 #, python-format
 msgid "Booking request for %s requires confirmation"
 msgstr ""
 
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:167
-#: ../wallace/module_invitationpolicy.py:377
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:240
+#: ../wallace/module_invitationpolicy.py:441
 #, python-format
 msgid "\"%(summary)s\" has been %(status)s"
 msgstr ""
@@ -2852,20 +3034,32 @@ 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:667
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:673
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:686
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:704
-#: ../wallace/module_invitationpolicy.py:955
+#. this should also trigger an update notification
+#. this should trigger an update notification
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:787
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:793
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:806
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:824
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:927
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:932
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:983
+#: ../wallace/module_invitationpolicy.py:1052
 #, python-format
 msgid "\"%s\" has been updated"
 msgstr ""
 
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:678
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:690
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:798
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:810
 msgid "PENDING"
 msgstr ""
 
+#. this should trigger a notification message
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:1003
+#: ../wallace/module_invitationpolicy.py:1110
+#, python-format
+msgid "\"%s\" has been cancelled"
+msgstr ""
+
 #: ../tests/unit/test-011-itip.py:408
 #, python-format
 msgid "Invitation for %(summary)s was %(status)s"
@@ -2913,21 +3107,21 @@ msgid "Could not write pid file %s"
 msgstr ""
 
 #: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60
-#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:61
+#: ../wallace/module_invitationpolicy.py:210 ../wallace/module_optout.py:61
 #: ../wallace/module_resources.py:125
 #, python-format
 msgid "Issuing callback after processing to stage %s"
 msgstr ""
 
 #: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61
-#: ../wallace/module_invitationpolicy.py:174 ../wallace/module_optout.py:62
+#: ../wallace/module_invitationpolicy.py:212 ../wallace/module_optout.py:62
 #: ../wallace/module_resources.py:131
 #, python-format
 msgid "Testing cb_action_%s()"
 msgstr ""
 
 #: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63
-#: ../wallace/module_invitationpolicy.py:176 ../wallace/module_optout.py:64
+#: ../wallace/module_invitationpolicy.py:214 ../wallace/module_optout.py:64
 #: ../wallace/module_resources.py:134
 #, python-format
 msgid "Attempting to execute cb_action_%s()"
@@ -2993,261 +3187,286 @@ msgstr ""
 msgid "An error occurred: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:158
+#: ../wallace/module_invitationpolicy.py:196
 #, python-format
 msgid "Invitation policy called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:215
-#: ../wallace/module_resources.py:176
+#: ../wallace/module_invitationpolicy.py:257
 #, python-format
-msgid "Failed to parse iTip events from message: %r"
+msgid "Failed to parse iTip objects from message: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:219
+#: ../wallace/module_invitationpolicy.py:261
 msgid ""
-"Message is not an iTip message or does not contain any (valid) iTip events."
+"Message is not an iTip message or does not contain any (valid) iTip objects."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:223
+#: ../wallace/module_invitationpolicy.py:265
 #, python-format
 msgid ""
-"iTip events attached to this message contain the following information: %r"
+"iTip objects attached to this message contain the following information: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:236
+#: ../wallace/module_invitationpolicy.py:278
 #, python-format
 msgid "No itips, no users, pass along %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:239
+#: ../wallace/module_invitationpolicy.py:281
 #, python-format
 msgid "iTips, but no users, pass along %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:259
+#: ../wallace/module_invitationpolicy.py:301
 #, python-format
 msgid "No user attendee matching envelope recipient %s, skip message"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:263
+#: ../wallace/module_invitationpolicy.py:305
 #, python-format
 msgid "Receiving user: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:287
+#: ../wallace/module_invitationpolicy.py:330
 #, python-format
 msgid "Apply invitation policy %r for sender %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:298
+#: ../wallace/module_invitationpolicy.py:341
 #, python-format
 msgid "Ignoring '%s' iTip method"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:302
+#: ../wallace/module_invitationpolicy.py:345
 #, python-format
 msgid "iTip message %r consumed by the invitationpolicy module"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:318
+#: ../wallace/module_invitationpolicy.py:361
 msgid "Pass invitation for manual processing"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:323
+#: ../wallace/module_invitationpolicy.py:366
 #, python-format
 msgid "Receiving Attendee: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:342
+#: ../wallace/module_invitationpolicy.py:386
+#, python-format
+msgid "Existing %s: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:397
 #, python-format
-msgid "Existing event: %r"
+msgid "Precondition for object %r fulfilled: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:353
+#: ../wallace/module_invitationpolicy.py:415
 #, python-format
-msgid "Precondition for event %r fulfilled: %r"
+msgid ""
+"The iTip request sequence (%r) doesn't match the referred object version (%"
+"r). Ignoring."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:389
+#: ../wallace/module_invitationpolicy.py:420
 #, python-format
-msgid "No RSVP for recipient %r requested"
+msgid "Auto-updating %s %r on iTip REQUEST (no re-scheduling)"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:415
+#: ../wallace/module_invitationpolicy.py:475
 msgid "Pass reply for manual processing"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:422
+#: ../wallace/module_invitationpolicy.py:482
 #, python-format
 msgid "Sender Attendee: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:434
+#: ../wallace/module_invitationpolicy.py:494
 #, python-format
 msgid ""
-"The iTip reply sequence (%r) doesn't match the referred event version (%r). "
+"The iTip reply sequence (%r) doesn't match the referred object version (%r). "
 "Forwarding to Inbox."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:440
+#: ../wallace/module_invitationpolicy.py:500
 #, python-format
-msgid "Auto-updating event %r on iTip REPLY"
+msgid "Auto-updating %s %r on iTip REPLY"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:465
+#: ../wallace/module_invitationpolicy.py:525
 #, python-format
 msgid "Add delegatee: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:468
+#: ../wallace/module_invitationpolicy.py:528
 #, python-format
 msgid "Update existing delegatee: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:473
+#: ../wallace/module_invitationpolicy.py:533
 #, python-format
 msgid "Update delegator: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:490
-#: ../wallace/module_invitationpolicy.py:519
+#: ../wallace/module_invitationpolicy.py:550
+#: ../wallace/module_invitationpolicy.py:582
 msgid ""
-"The event referred by this reply was not found in the user's calendars. "
+"The object referred by this reply was not found in the user's folders. "
 "Forwarding to Inbox."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:503
+#: ../wallace/module_invitationpolicy.py:563
 msgid "Pass cancellation for manual processing"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:548
+#: ../wallace/module_invitationpolicy.py:611
 #, python-format
 msgid "Checking if email address %r belongs to a local user"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:553
+#: ../wallace/module_invitationpolicy.py:616
 #, python-format
 msgid "User DN: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:555
+#: ../wallace/module_invitationpolicy.py:618
 #, python-format
 msgid "No user record(s) found for %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:608
+#: ../wallace/module_invitationpolicy.py:674
 #, python-format
 msgid "User record doesn't have the mailbox attribute %r set"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:621
+#: ../wallace/module_invitationpolicy.py:687
 #, python-format
 msgid "IMAP proxy authentication failed: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:643
+#: ../wallace/module_invitationpolicy.py:709
 #, python-format
-msgid "List calendar folders for user %r: %r"
+msgid "List %r folders for user %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:659
+#: ../wallace/module_invitationpolicy.py:725
 #, python-format
 msgid "IMAP metadata for %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:689
+#: ../wallace/module_invitationpolicy.py:755
 #, python-format
-msgid "Searching folder %r for event %r"
+msgid "Searching folder %r for %s %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:701
+#: ../wallace/module_invitationpolicy.py:771
 #, python-format
-msgid "Failed to parse event from message %s/%s: %s"
+msgid "Failed to parse %s from message %s/%s: %s"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:727
+#: ../wallace/module_invitationpolicy.py:797
 #, python-format
 msgid "Listing events from folder %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:740
-#: ../wallace/module_resources.py:553 ../wallace/module_resources.py:601
+#: ../wallace/module_invitationpolicy.py:810
+#: ../wallace/module_resources.py:566 ../wallace/module_resources.py:614
 #, python-format
 msgid "Failed to parse event from message %s/%s: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:746
+#: ../wallace/module_invitationpolicy.py:816
 #, python-format
 msgid "Existing event %r conflicts with invitation %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:753
-#: ../wallace/module_resources.py:411
+#: ../wallace/module_invitationpolicy.py:823
+#: ../wallace/module_resources.py:419
 #, python-format
 msgid "start: %r, end: %r, total: %r, messages: %d"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:779
+#: ../wallace/module_invitationpolicy.py:849
 #, python-format
 msgid "%r is locked, waiting..."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:842
+#: ../wallace/module_invitationpolicy.py:913
 #, python-format
-msgid "Failed to save event: no calendar folder found for user %r"
+msgid "Failed to save %s: no target folder found for user %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:845
+#: ../wallace/module_invitationpolicy.py:916
 #, python-format
-msgid "Save event %r to user calendar %r"
+msgid "Save %s %r to user folder %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:858
+#: ../wallace/module_invitationpolicy.py:929
 #, python-format
-msgid "Failed to save event to user calendar at %r: %r"
+msgid "Failed to save %s to user folder at %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:874
+#: ../wallace/module_invitationpolicy.py:945
 #, python-format
-msgid "Delete event %r in %r: %r"
+msgid "Delete %s %r in %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:894
+#: ../wallace/module_invitationpolicy.py:970
 #, python-format
-msgid "Compose participation status summary for event %r to user %r"
+msgid "Compose participation status summary for %s %r to user %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:931
+#: ../wallace/module_invitationpolicy.py:1003
 #, python-format
 msgid ""
 "Waiting for more automated replies (got %d of %d); skipping notification"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1028
+#: ../wallace/module_invitationpolicy.py:1013
+#, python-format
+msgid "Changes submitted by %s have been automatically applied."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1022
+msgid "(removed)"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1045
+#: ../wallace/module_invitationpolicy.py:1103
+#: ../wallace/module_invitationpolicy.py:1193
+msgid "*** This is an automated message. Please do not reply. ***"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1076
+#, python-format
+msgid "Send cancellation notification for %s %r to user %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1183
 #, python-format
 msgid "Updated %s's copy of %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1031
+#: ../wallace/module_invitationpolicy.py:1186
 #, python-format
 msgid "Attendee %s's copy of %r not found"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1034
+#: ../wallace/module_invitationpolicy.py:1189
 #, python-format
 msgid "Attendee %r not found in LDAP"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1038
+#: ../wallace/module_invitationpolicy.py:1196
 #, 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"
-"    "
+msgid "%(name)s has %(status)s your assignment for %(summary)s."
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:1198
+#, python-format
+msgid "%(name)s has %(status)s your invitation for %(summary)s."
 msgstr ""
 
 #. modules.next_module('optout')
@@ -3276,220 +3495,225 @@ msgstr ""
 msgid "Resource Management called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:181
+#: ../wallace/module_resources.py:180
+#, python-format
+msgid "Failed to parse iTip events from message: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:185
 msgid "Message is not an iTip message or does not contain any "
 msgstr ""
 
-#: ../wallace/module_resources.py:189
+#: ../wallace/module_resources.py:193
 msgid "iTip events attached to this message contain the "
 msgstr ""
 
-#: ../wallace/module_resources.py:219
+#: ../wallace/module_resources.py:226
 msgid "Not an iTip message, but sent to resource nonetheless. Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:227
+#: ../wallace/module_resources.py:234
 #, python-format
 msgid "No itips, no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:230
+#: ../wallace/module_resources.py:237
 #, python-format
 msgid "iTips, but no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:239
+#: ../wallace/module_resources.py:246
 #, python-format
 msgid "No resource attendees matching envelope recipient %s, Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:249
+#: ../wallace/module_resources.py:256
 #, python-format
 msgid "Resources: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:267
+#: ../wallace/module_resources.py:274
 #, python-format
 msgid "Sender Attendee: %r => %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:274
+#: ../wallace/module_resources.py:281
 #, python-format
 msgid ""
 "The iTip reply sequence (%r) doesn't match the referred event version (%r). "
 "Ignoring."
 msgstr ""
 
-#: ../wallace/module_resources.py:299
+#: ../wallace/module_resources.py:306
 #, python-format
 msgid "Event referenced by this REPLY (%r) not found in resource calendar"
 msgstr ""
 
-#: ../wallace/module_resources.py:302
+#: ../wallace/module_resources.py:309
 msgid "No event reference found in this REPLY. Ignoring."
 msgstr ""
 
-#: ../wallace/module_resources.py:311
+#: ../wallace/module_resources.py:318
 #, python-format
 msgid "Receiving Resource: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:319
+#: ../wallace/module_resources.py:326
 #, python-format
 msgid "Recipient %r is non-participant, ignoring message"
 msgstr ""
 
-#: ../wallace/module_resources.py:346
+#: ../wallace/module_resources.py:354
 #, python-format
 msgid "Accept invitation for individual resource %r / %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:375
+#: ../wallace/module_resources.py:383
 #, python-format
 msgid "Delegate invitation for resource collection %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:407
+#: ../wallace/module_resources.py:415
 #, python-format
 msgid "Failed to read resource calendar for %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:417
+#: ../wallace/module_resources.py:425
 #, python-format
 msgid "Polling for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:420
+#: ../wallace/module_resources.py:428
 #, python-format
 msgid "Resource %r has been popped from the list"
 msgstr ""
 
-#: ../wallace/module_resources.py:424
+#: ../wallace/module_resources.py:432
 msgid "Resource is a collection"
 msgstr ""
 
-#: ../wallace/module_resources.py:435
+#: ../wallace/module_resources.py:443
 #, python-format
 msgid "Removed conflicting resources from %r: (%r) => %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:447
+#: ../wallace/module_resources.py:455
 #, python-format
 msgid "Conflicting events: %r for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:464
+#: ../wallace/module_resources.py:474
 #, python-format
 msgid "Delegate to another resource collection member: %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:526
+#: ../wallace/module_resources.py:536
 #, python-format
 msgid "Checking events in resource folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:542
+#: ../wallace/module_resources.py:555
 #, python-format
 msgid "Fetching message UID %r from folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:565
+#: ../wallace/module_resources.py:578
 #, python-format
 msgid "Event %r conflicts with event %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:586
+#: ../wallace/module_resources.py:599
 #, python-format
 msgid "Searching %r for event %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:592
+#: ../wallace/module_resources.py:605
 #, python-format
 msgid "Failed to access resource calendar:: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:621
+#: ../wallace/module_resources.py:634
 #, python-format
 msgid "Apply invitation policies %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:640
+#: ../wallace/module_resources.py:653
 #, python-format
 msgid "Adding event to %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:694
+#: ../wallace/module_resources.py:707
 #, python-format
 msgid "Failed to save event to resource calendar at %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:711
+#: ../wallace/module_resources.py:724
 #, python-format
 msgid "Delete resource calendar object %r in %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:754
+#: ../wallace/module_resources.py:767
 #, python-format
 msgid "Checking if email address %r belongs to a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:762 ../wallace/module_resources.py:836
-#: ../wallace/module_resources.py:870
+#: ../wallace/module_resources.py:775 ../wallace/module_resources.py:849
+#: ../wallace/module_resources.py:883
 #, python-format
 msgid "Resource record(s): %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:764 ../wallace/module_resources.py:838
-#: ../wallace/module_resources.py:873
+#: ../wallace/module_resources.py:777 ../wallace/module_resources.py:851
+#: ../wallace/module_resources.py:886
 #, python-format
 msgid "No resource (collection) records found for %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:768 ../wallace/module_resources.py:842
-#: ../wallace/module_resources.py:877
+#: ../wallace/module_resources.py:781 ../wallace/module_resources.py:855
+#: ../wallace/module_resources.py:890
 #, python-format
 msgid "Resource record: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:788
+#: ../wallace/module_resources.py:801
 #, python-format
 msgid "Raw itip_events: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:796
+#: ../wallace/module_resources.py:809
 #, python-format
 msgid "Raw set of attendees: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:804
+#: ../wallace/module_resources.py:817
 #, python-format
 msgid "Raw set of resources: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:809
+#: ../wallace/module_resources.py:822
 #, python-format
 msgid "Raw set of organizers: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:829
+#: ../wallace/module_resources.py:842
 #, python-format
 msgid "Checking if attendee %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:845 ../wallace/module_resources.py:879
+#: ../wallace/module_resources.py:858 ../wallace/module_resources.py:892
 msgid "Resource reservation made but no resource records found"
 msgstr ""
 
-#: ../wallace/module_resources.py:864
+#: ../wallace/module_resources.py:877
 #, python-format
 msgid "Checking if resource %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:882
+#: ../wallace/module_resources.py:895
 msgid "The following resources are being referred to in the "
 msgstr ""
 
-#: ../wallace/module_resources.py:1047
+#: ../wallace/module_resources.py:1060
 #, python-format
 msgid ""
 "\n"
@@ -3500,7 +3724,7 @@ msgid ""
 "            "
 msgstr ""
 
-#: ../wallace/module_resources.py:1066
+#: ../wallace/module_resources.py:1079
 #, python-format
 msgid ""
 "\n"
@@ -3510,7 +3734,7 @@ msgid ""
 "    "
 msgstr ""
 
-#: ../wallace/module_resources.py:1073
+#: ../wallace/module_resources.py:1086
 #, python-format
 msgid ""
 "\n"
@@ -3519,16 +3743,16 @@ msgid ""
 "        "
 msgstr ""
 
-#: ../wallace/module_resources.py:1104
+#: ../wallace/module_resources.py:1117
 #, python-format
 msgid "Sending booking notification for event %r to %r from %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:1121
+#: ../wallace/module_resources.py:1134
 msgid "failed"
 msgstr ""
 
-#: ../wallace/module_resources.py:1140
+#: ../wallace/module_resources.py:1153
 #, python-format
 msgid ""
 "\n"
@@ -3540,7 +3764,7 @@ msgid ""
 "        "
 msgstr ""
 
-#: ../wallace/module_resources.py:1146
+#: ../wallace/module_resources.py:1159
 #, python-format
 msgid ""
 "\n"
@@ -3554,12 +3778,12 @@ msgid ""
 "        "
 msgstr ""
 
-#: ../wallace/module_resources.py:1190
+#: ../wallace/module_resources.py:1203
 #, python-format
 msgid "Clone invitation for owner confirmation: %r from %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:1196
+#: ../wallace/module_resources.py:1209
 #, python-format
 msgid ""
 "\n"


commit fd68e0f4527f27fb406861036108d44cf500612e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Aug 22 15:12:45 2014 -0400

    List event/task properties changes in update notification mails (#3447)

diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 3ca52b2..2c99717 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -19,6 +19,8 @@ from todo import todo_from_ical
 from todo import todo_from_string
 from todo import todo_from_message
 
+from utils import property_label
+from utils import property_to_string
 from utils import compute_diff
 from utils import to_dt
 
@@ -35,6 +37,8 @@ __all__ = [
         "todo_from_ical",
         "todo_from_string",
         "todo_from_message",
+        "property_label",
+        "property_to_string",
         "compute_diff",
         "to_dt",
     ]
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 24b026e..7b9e13c 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -365,7 +365,12 @@ 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'):
+    def get_date_text(self, date_format=None, time_format=None):
+        if date_format is None:
+            date_format = _("%Y-%m-%d")
+        if time_format is None:
+            time_format = _("%H:%M (%Z)")
+
         start = self.get_start()
         end = self.get_end()
         all_day = not hasattr(start, 'date')
diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py
index 4a0b6c5..37474b1 100644
--- a/pykolab/xml/recurrence_rule.py
+++ b/pykolab/xml/recurrence_rule.py
@@ -1,6 +1,9 @@
 import kolabformat
 from pykolab.xml import utils as xmlutils
 
+from pykolab.translate import _
+from pykolab.translate import N_
+
 """
     def setFrequency(self, *args): return _kolabformat.RecurrenceRule_setFrequency(self, *args)
     def frequency(self): return _kolabformat.RecurrenceRule_frequency(self)
@@ -31,6 +34,20 @@ from pykolab.xml import utils as xmlutils
     def isValid(self): return _kolabformat.RecurrenceRule_isValid(self)
 """
 
+frequency_labels = {
+    "YEARLY":   N_("Every %d year(s)"),
+    "MONTHLY":  N_("Every %d month(s)"),
+    "WEEKLY":   N_("Every %d week(s)"),
+    "DAILY":    N_("Every %d day(s)"),
+    "HOURLY":   N_("Every %d hours"),
+    "MINUTELY": N_("Every %d minutes"),
+    "SECONDLY": N_("Every %d seconds")
+}
+
+def frequency_label(freq):
+    return _(frequency_labels[freq]) if frequency_labels.has_key(freq) else _(freq)
+
+
 class RecurrenceRule(kolabformat.RecurrenceRule):
     frequency_map = {
         None: kolabformat.RecurrenceRule.FreqNone,
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index 35d7578..2fe82d2 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -4,6 +4,9 @@ import kolabformat
 from dateutil.tz import tzlocal
 from collections import OrderedDict
 
+from pykolab.translate import _
+from pykolab.translate import N_
+
 
 def to_dt(dt):
     """
@@ -113,6 +116,113 @@ def to_cdatetime(_datetime, with_timezone=True, as_utc=False):
     return _cdatetime
 
 
+property_labels = {
+    "name":        N_("Name"),
+    "summary":     N_("Summary"),
+    "location":    N_("Location"),
+    "description": N_("Description"),
+    "url":         N_("URL"),
+    "status":      N_("Status"),
+    "priority":    N_("Priority"),
+    "attendee":    N_("Attendee"),
+    "start":       N_("Start"),
+    "end":         N_("End"),
+    "due":         N_("Due"),
+    "rrule":       N_("Repeat"),
+    "exdate":      N_("Repeat Exception"),
+    "organizer":   N_("Organizer"),
+    "attach":      N_("Attachment"),
+    "alarm":       N_("Alarm"),
+    "classification":   N_("Classification"),
+    "percent-complete": N_("Progress")
+}
+
+def property_label(propname):
+    """
+        Return a localized name for the given object property
+    """
+    return _(property_labels[propname]) if property_labels.has_key(propname) else _(propname)
+
+
+def property_to_string(propname, value):
+    """
+        Render a human readable string for the given object property
+    """
+    date_format = _("%Y-%m-%d")
+    time_format = _("%H:%M (%Z)")
+    date_time_format = date_format + " " + time_format
+    maxlen = 50
+
+    if isinstance(value, datetime.datetime):
+        return value.strftime(date_time_format)
+    elif isinstance(value, datetime.date):
+        return value.strftime(date_format)
+    elif isinstance(value, int):
+        return str(value)
+    elif isinstance(value, str):
+        if len(value) > maxlen:
+            return value[:maxlen].rsplit(' ', 1)[0] + '...'
+        return value
+    elif isinstance(value, object) and hasattr(value, 'to_dict'):
+        value = value.to_dict()
+
+    if isinstance(value, dict):
+        if propname == 'attendee':
+            from . import attendee
+            name = value['name'] if value.has_key('name') and not value['name'] == '' else value['email']
+            return "%s, %s" % (name, attendee.participant_status_label(value['partstat']))
+
+        elif propname == 'organizer':
+            return value['name'] if value.has_key('name') and not value['name'] == '' else value['email']
+
+        elif propname == 'rrule':
+            from . import recurrence_rule
+            rrule = recurrence_rule.frequency_label(value['frequency']) % (value['interval'])
+            if value.has_key('count') and value['count'] > 0:
+                rrule += " " + _("for %d times") % (value['count'])
+            elif value.has_key('until') and (isinstance(value['until'], datetime.datetime) or isinstance(value['until'], datetime.date)):
+                rrule += " " + _("until %s") % (value['until'].strftime(date_format))
+            return rrule
+
+        elif propname == 'alarm':
+            alarm_type_labels = {
+                'DISPLAY': _("Display message"),
+                'EMAIL':   _("Send email"),
+                'AUDIO':   _("Play sound")
+            }
+            alarm = alarm_type_labels.get(value['action'], "")
+            if isinstance(value['trigger'], datetime.datetime):
+                alarm += " @ " + property_to_string('trigger', value['trigger'])
+            else:
+                rel = _("%s after") if value['trigger']['related'] == 'END' else _("%s before")
+                offsets = []
+                try:
+                    from icalendar import vDuration
+                    duration = vDuration.from_ical(value['trigger']['value'].strip('-'))
+                except:
+                    return None
+
+                if duration.days:
+                    offsets.append(_("%d day(s)") % (duration.days))
+                if duration.seconds:
+                    hours = duration.seconds // 3600
+                    minutes = duration.seconds % 3600 // 60
+                    seconds = duration.seconds % 60
+                    if hours:
+                        offsets.append(_("%d hour(s)") % (hours))
+                    if minutes or (hours and seconds):
+                        offsets.append(_("%d minute(s)") % (minutes))
+                if len(offsets):
+                    alarm += " " + rel % (", ".join(offsets))
+
+            return alarm
+
+        elif propname == 'attach':
+            return value['label'] if value.has_key('label') else value['fmttype']
+
+    return None
+
+
 def compute_diff(a, b, reduced=False):
     """
         List the differences between two given dicts
@@ -137,7 +247,7 @@ def compute_diff(a, b, reduced=False):
             while index < length:
                 aai = aa[index] if index < len(aa) else None
                 bbi = bb[index] if index < len(bb) else None
-                if not aai == bbi:
+                if not compare_values(aai, bbi):
                     if reduced:
                         (old, new) = reduce_properties(aai, bbi)
                     else:
@@ -146,7 +256,7 @@ def compute_diff(a, b, reduced=False):
                 index += 1
 
         # the two properties differ
-        elif not aa.__class__ == bb.__class__ or not aa == bb:
+        elif not compare_values(aa, bb):
             if reduced:
                 (old, new) = reduce_properties(aa, bb)
             else:
@@ -156,6 +266,22 @@ def compute_diff(a, b, reduced=False):
     return diff
 
 
+def compare_values(aa, bb):
+    ignore_keys = ['rsvp']
+    if not aa.__class__ == bb.__class__:
+        return False
+
+    if isinstance(aa, dict) and isinstance(bb, dict):
+        aa = dict(aa)
+        bb = dict(bb)
+        # ignore some properties for comparison
+        for k in ignore_keys:
+            aa.pop(k, None)
+            bb.pop(k, None)
+
+    return aa == bb
+
+
 def reduce_properties(aa, bb):
     """
         Compares two given structs and removes equal values in bb
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index 8feeff0..c3be462 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -967,7 +967,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIsInstance(todo, pykolab.xml.Todo)
 
         # send a reply from jane to john
-        partstat = 'DECLINED'
+        partstat = 'COMPLETED'
         self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=due, template=itip_todo_reply, partstat=partstat)
 
         # check for the updated task in john's tasklist
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 6a9fd4f..7124e0c 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -5,6 +5,7 @@ import sys
 import unittest
 import kolabformat
 import icalendar
+import pykolab
 
 from pykolab.xml import Attendee
 from pykolab.xml import Event
@@ -15,6 +16,7 @@ from pykolab.xml import event_from_ical
 from pykolab.xml import event_from_string
 from pykolab.xml import event_from_message
 from pykolab.xml import compute_diff
+from pykolab.xml import property_to_string
 from collections import OrderedDict
 
 ical_event = """
@@ -247,6 +249,17 @@ xml_event = """
 class TestEventXML(unittest.TestCase):
     event = Event()
 
+    @classmethod
+    def setUp(self):
+        """ Compatibility for twisted.trial.unittest
+        """
+        self.setup_class()
+
+    @classmethod
+    def setup_class(self, *args, **kw):
+        # set language to default
+        pykolab.translate.setUserLanguage('en_US')
+
     def assertIsInstance(self, _value, _type):
         if hasattr(unittest.TestCase, 'assertIsInstance'):
             return unittest.TestCase.assertIsInstance(self, _value, _type)
@@ -640,6 +653,18 @@ END:VEVENT
         self.assertEqual(pa['new'], dict(partstat='DECLINED'))
 
 
+    def test_026_property_to_string(self):
+        data = event_from_string(xml_event).to_dict()
+        self.assertEqual(property_to_string('sequence', data['sequence']), "1")
+        self.assertEqual(property_to_string('start', data['start']), "2014-08-13 10:00 (GMT)")
+        self.assertEqual(property_to_string('organizer', data['organizer']), "Doe, John")
+        self.assertEqual(property_to_string('attendee', data['attendee'][0]), "jane at example.org, Accepted")
+        self.assertEqual(property_to_string('rrule', data['rrule']), "Every 1 day(s) until 2014-07-25")
+        self.assertEqual(property_to_string('exdate', data['exdate'][0]), "2014-07-19")
+        self.assertEqual(property_to_string('alarm', data['alarm'][0]), "Display message 2 hour(s) before")
+        self.assertEqual(property_to_string('attach', data['attach'][0]), "noname.1395223627.5555")
+
+
     def _find_prop_in_list(self, diff, name):
         for prop in diff:
             if prop['property'] == name:
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 9ba1490..5c187c5 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,6 +41,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 utils as xmlutils
 from pykolab.xml import todo_from_message
 from pykolab.xml import event_from_message
 from pykolab.xml import participant_status_label
@@ -237,6 +238,10 @@ def execute(*args, **kw):
     # parse full message
     message = Parser().parse(open(filepath, 'r'))
 
+    # invalid message, skip
+    if not message.get('X-Kolab-To'):
+        return filepath
+
     recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))]
     sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0]
 
@@ -421,7 +426,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             itip_event['xml'].set_percentcomplete(existing.get_percentcomplete())
 
         if policy & COND_NOTIFY:
-            send_update_notification(itip_event['xml'], receiving_user, False)
+            send_update_notification(itip_event['xml'], receiving_user, existing, False)
 
     # if RSVP, send an iTip REPLY
     if rsvp or scheduling_required:
@@ -533,7 +538,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
             # update the organizer's copy of the object
             if update_object(existing, receiving_user):
                 if policy & COND_NOTIFY:
-                    send_update_notification(existing, receiving_user, True)
+                    send_update_notification(existing, receiving_user, existing, True)
 
                 # update all other attendee's copies
                 if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
@@ -947,7 +952,7 @@ def delete_object(existing):
     imap.imap.m.expunge()
 
 
-def send_update_notification(object, receiving_user, reply=True):
+def send_update_notification(object, receiving_user, old=None, reply=True):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
@@ -1005,8 +1010,18 @@ def send_update_notification(object, receiving_user, reply=True):
             if len(attendees) > 0:
                 roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
     else:
-        # TODO: compose a diff of changes to previous version
-        roundup = "\n" + _("Minor changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
+        roundup = "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
+
+        # list properties changed from previous version
+        if old:
+            diff = xmlutils.compute_diff(old.to_dict(), object.to_dict())
+            if len(diff) > 1:
+                roundup += "\n"
+                for change in diff:
+                    if not change['property'] in ['created','lastmodified-date','sequence']:
+                        new_value = xmlutils.property_to_string(change['property'], change['new']) if change['new'] else _("(removed)")
+                        if new_value:
+                            roundup += "\n- %s: %s" % (xmlutils.property_label(change['property']), new_value)
 
     # compose different notification texts for events/tasks
     if object.type == 'task':
@@ -1023,7 +1038,7 @@ def send_update_notification(object, receiving_user, reply=True):
             %(roundup)s
         """ % {
             'summary': object.get_summary(),
-            'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+            'start': xmlutils.property_to_string('start', object.get_start()),
             'roundup': roundup
         }
 
@@ -1081,7 +1096,7 @@ def send_cancel_notification(object, receiving_user):
             The copy in your calendar as been marked as cancelled accordingly.
         """ % {
             'summary': object.get_summary(),
-            'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+            'start': xmlutils.property_to_string('start', object.get_start()),
             'organizer': orgname if orgname else orgemail
         }
 


commit 3231cd8408132d3f7ddb3ac1626a049474101101
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Aug 22 13:06:58 2014 -0400

    Map additional partstat values for Todos

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index cdfe86a..10fd006 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -19,9 +19,8 @@ participant_status_labels = {
         kolabformat.PartDeclined: N_("Declined"),
         kolabformat.PartTentative: N_("Tentatively Accepted"),
         kolabformat.PartDelegated: N_("Delegated"),
-        # waiting for libkolabxml to support these (#3472)
-        #kolabformat.PartCompleted: N_("Completed"),
-        #kolabformat.PartInProcess: N_("Started"),
+        kolabformat.PartCompleted: N_("Completed"),
+        kolabformat.PartInProcess: N_("Started"),
     }
 
 def participant_status_label(status):
@@ -41,9 +40,8 @@ class Attendee(kolabformat.Attendee):
             "DECLINED": kolabformat.PartDeclined,
             "TENTATIVE": kolabformat.PartTentative,
             "DELEGATED": kolabformat.PartDelegated,
-            # waiting for libkolabxml to support these (#3472)
-            #"COMPLETED": kolabformat.PartCompleted,
-            #"IN-PROCESS": kolabformat.PartInProcess,
+            "COMPLETED": kolabformat.PartCompleted,
+            "IN-PROCESS": kolabformat.PartInProcess,
         }
 
     # See RFC 2445, 5445


commit b34c2a611bb3e76526d17a651fae4aa669ba8f43
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Aug 22 13:05:53 2014 -0400

    Basic sanity check for input message

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index aa3c473..0eb4659 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -160,6 +160,10 @@ def execute(*args, **kw):
     # parse full message
     message = Parser().parse(open(filepath, 'r'))
 
+    # invalid message, skip
+    if not message.get('X-Kolab-To'):
+        return filepath
+
     recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))]
     sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0]
 


commit 38a99ecd5b487fe47c84c970a1bb50dd5627735d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Aug 22 11:51:51 2014 -0400

    Add utility function to compute diffs between two objects (converted to dicts)

diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 20b7e9f..3ca52b2 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -19,6 +19,7 @@ from todo import todo_from_ical
 from todo import todo_from_string
 from todo import todo_from_message
 
+from utils import compute_diff
 from utils import to_dt
 
 __all__ = [
@@ -34,6 +35,7 @@ __all__ = [
         "todo_from_ical",
         "todo_from_string",
         "todo_from_message",
+        "compute_diff",
         "to_dt",
     ]
 
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index aa05e11..35d7578 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -2,6 +2,8 @@ import datetime
 import pytz
 import kolabformat
 from dateutil.tz import tzlocal
+from collections import OrderedDict
+
 
 def to_dt(dt):
     """
@@ -109,3 +111,68 @@ def to_cdatetime(_datetime, with_timezone=True, as_utc=False):
         _cdatetime.setUTC(True)
 
     return _cdatetime
+
+
+def compute_diff(a, b, reduced=False):
+    """
+        List the differences between two given dicts
+    """
+    diff = []
+
+    properties = a.keys()
+    properties.extend([x for x in b.keys() if x not in properties])
+
+    for prop in properties:
+        aa = a[prop] if a.has_key(prop) else None
+        bb = b[prop] if b.has_key(prop) else None
+
+        # compare two lists
+        if isinstance(aa, list) or isinstance(bb, list):
+            if not isinstance(aa, list):
+                aa = [aa]
+            if not isinstance(bb, list):
+                bb = [bb]
+            index = 0
+            length = max(len(aa), len(bb))
+            while index < length:
+                aai = aa[index] if index < len(aa) else None
+                bbi = bb[index] if index < len(bb) else None
+                if not aai == bbi:
+                    if reduced:
+                        (old, new) = reduce_properties(aai, bbi)
+                    else:
+                        (old, new) = (aai, bbi)
+                    diff.append(OrderedDict([('property', prop), ('index', index), ('old', old), ('new', new)]))
+                index += 1
+
+        # the two properties differ
+        elif not aa.__class__ == bb.__class__ or not aa == bb:
+            if reduced:
+                (old, new) = reduce_properties(aa, bb)
+            else:
+                (old, new) = (aa, bb)
+            diff.append(OrderedDict([('property', prop), ('old', old), ('new', new)]))
+
+    return diff
+
+
+def reduce_properties(aa, bb):
+    """
+        Compares two given structs and removes equal values in bb
+    """
+    if not isinstance(aa, dict) or not isinstance(bb, dict):
+        return (aa, bb)
+
+    properties = aa.keys()
+    properties.extend([x for x in bb.keys() if x not in properties])
+
+    for prop in properties:
+        if not aa.has_key(prop) or not bb.has_key(prop):
+            continue
+        if isinstance(aa[prop], dict) and isinstance(bb[prop], dict):
+            (aa[prop], bb[prop]) = reduce_properties(aa[prop], bb[prop])
+        if aa[prop] == bb[prop]:
+            # del aa[prop]
+            del bb[prop]
+
+    return (aa, bb)
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index d9e05fa..6a9fd4f 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -14,6 +14,8 @@ 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
+from pykolab.xml import compute_diff
+from collections import OrderedDict
 
 ical_event = """
 BEGIN:VEVENT
@@ -223,7 +225,7 @@ xml_event = """
                 <text>alarm 2</text>
               </description>
               <attendee>
-                  <cal-address>mailto:%3Cjohn.die%40example.org%3E</cal-address>
+                  <cal-address>mailto:%3Cjohn.doe%40example.org%3E</cal-address>
               </attendee>
               <trigger>
                 <parameters>
@@ -615,6 +617,35 @@ END:VEVENT
         self.assertEqual(data['alarm'][1]['trigger']['value'], '-P1D')
         self.assertEqual(len(data['alarm'][1]['attendee']), 1)
 
+    def test_026_compute_diff(self):
+        e1 = event_from_string(xml_event)
+        e2 = event_from_string(xml_event)
+
+        e2.set_summary("test2")
+        e2.set_end(e1.get_end() + datetime.timedelta(hours=2))
+        e2.set_sequence(e1.get_sequence() + 1)
+        e2.set_attendee_participant_status("jane at example.org", "DECLINED")
+        e2.set_lastmodified()
+
+        diff = compute_diff(e1.to_dict(), e2.to_dict(), True)
+        self.assertEqual(len(diff), 5)
+
+        ps = self._find_prop_in_list(diff, 'summary')
+        self.assertIsInstance(ps, OrderedDict)
+        self.assertEqual(ps['new'], "test2")
+
+        pa = self._find_prop_in_list(diff, 'attendee')
+        self.assertIsInstance(pa, OrderedDict)
+        self.assertEqual(pa['index'], 0)
+        self.assertEqual(pa['new'], dict(partstat='DECLINED'))
+
+
+    def _find_prop_in_list(self, diff, name):
+        for prop in diff:
+            if prop['property'] == name:
+                return prop
+        return None
+
 
 if __name__ == '__main__':
     unittest.main()


commit afdbc23d4b157a8e5449531896e0e0f01ce6fc4b
Author: Aeneas Jaißle <aj at ajaissle.de>
Date:   Sun Aug 24 14:04:57 2014 +0200

    Actually use the socket configured

diff --git a/saslauthd/__init__.py b/saslauthd/__init__.py
index b7f81d5..6590747 100644
--- a/saslauthd/__init__.py
+++ b/saslauthd/__init__.py
@@ -170,13 +170,13 @@ class SASLAuthDaemon(object):
 
         # TODO: The saslauthd socket path could be a setting.
         try:
-            os.remove('/var/run/saslauthd/mux')
+            os.remove(socketfile)
         except:
             # TODO: Do the "could not remove, could not start" dance
             pass
 
-        s.bind('/var/run/saslauthd/mux')
-        os.chmod('/var/run/saslauthd/mux', 0777)
+        s.bind(socketfile)
+        os.chmod(socketfile, 0777)
 
         s.listen(5)
 
@@ -271,7 +271,7 @@ class SASLAuthDaemon(object):
 
     def _ensure_socket_dir(self):
         utils.ensure_directory(
-                '/var/run/saslauthd/',
+                os.path.dirname(socketfile),
                 conf.process_username,
                 conf.process_groupname
             )


commit 3aa398fd8c4836868d5223638d3318bd64c337b6
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 21 10:19:34 2014 +0200

    Correct assets path

diff --git a/share/templates/roundcubemail/config.inc.php.tpl b/share/templates/roundcubemail/config.inc.php.tpl
index 920423e..61214f2 100644
--- a/share/templates/roundcubemail/config.inc.php.tpl
+++ b/share/templates/roundcubemail/config.inc.php.tpl
@@ -7,7 +7,7 @@
     \$config['des_key'] = "$des_key";
     \$config['username_domain'] = '$primary_domain';
     \$config['use_secure_urls'] = true;
-    \$config['assets_path'] = '/roundcubemail/assets/';
+    \$config['assets_path'] = '/assets/';
 
     \$config['mail_domain'] = '';
 


commit 44bde53ddb4eadd4fc3fd653fcefe48c39285d5a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 21 13:46:18 2014 -0400

    Apply ACT_UPDATE policy on iTip REQUESTs with no re-scheduling (i.e. unchanged sequence number) (#3447)

diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index dffc9df..8feeff0 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -387,13 +387,15 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None):
+    def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None, folder=None):
         if start is None:
             start = datetime.datetime.now(pytz.timezone("Europe/Berlin"))
         if user is None:
             user = self.john
         if attendees is None:
             attendees = [self.jane]
+        if folder is None:
+            folder = user['kolabcalendarfolder']
 
         end = start + datetime.timedelta(hours=4)
 
@@ -419,7 +421,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         imap = IMAP()
         imap.connect()
 
-        mailbox = imap.folder_quote(user['kolabcalendarfolder'])
+        mailbox = imap.folder_quote(folder)
         imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
         imap.imap.m.select(mailbox)
 
@@ -904,7 +906,34 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
 
 
-    def test_011_task_assignment_accept(self):
+    def test_011_manual_schedule_auto_update(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        # create an event in john's calendar as it was manually accepted
+        start = datetime.datetime(2014,9,2, 11,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_calendar_event(start, user=self.jane, sequence=1, folder=self.john['kolabcalendarfolder'])
+
+        # send update with the same sequence: no re-scheduling
+        templ = itip_invitation.replace("RSVP=TRUE", "RSVP=FALSE").replace("Doe, John", self.jane['displayname']).replace("john.doe at example.org", self.jane['mail'])
+        self.send_itip_update(self.john['mail'], uid, start, summary="test updated", sequence=1, partstat='ACCEPTED', template=templ)
+
+        time.sleep(10)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
+        self.assertIsInstance(event, pykolab.xml.Event)
+        self.assertEqual(event.get_summary(), "test updated")
+        self.assertEqual(event.get_attendee(self.john['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+        # this should also trigger an update notification
+        notification = self.check_message_received(_('"%s" has been updated') % ('test updated'), self.jane['mail'], mailbox=self.john['mailbox'])
+        self.assertIsInstance(notification, email.message.Message)
+
+        # send outdated update: should not be saved
+        self.send_itip_update(self.john['mail'], uid, start, summary="old test", sequence=0, partstat='NEEDS-ACTION', template=templ)
+        notification = self.check_message_received(_('"%s" has been updated') % ('old test'), self.jane['mail'], mailbox=self.john['mailbox'])
+        self.assertEqual(notification, None)
+
+
+    def test_020_task_assignment_accept(self):
         start = datetime.datetime(2014,9,10, 19,0,0)
         uid = self.send_itip_invitation(self.jane['mail'], start, summary='work', template=itip_todo)
 
@@ -928,7 +957,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(todo.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
 
 
-    def test_012_task_assignment_reply(self):
+    def test_021_task_assignment_reply(self):
         self.purge_mailbox(self.john['mailbox'])
 
         due = datetime.datetime(2014,9,12, 14,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
@@ -958,7 +987,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertIn(participant_status_label(partstat), notification_text)
 
 
-    def test_013_task_cancellation(self):
+    def test_022_task_cancellation(self):
         uid = self.send_itip_invitation(self.jane['mail'], summary='more work', template=itip_todo)
 
         time.sleep(10)
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index e753c38..9ba1490 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -403,6 +403,26 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             # TODO: delegate (but to whom?)
             return None
 
+    # auto-update changes if enabled for this user
+    elif policy & ACT_UPDATE and existing:
+        # compare sequence number to avoid outdated updates
+        if not itip_event['sequence'] == existing.get_sequence():
+            log.info(_("The iTip request sequence (%r) doesn't match the referred object version (%r). Ignoring.") % (
+                itip_event['sequence'], existing.get_sequence()
+            ))
+            return None
+
+        log.debug(_("Auto-updating %s %r on iTip REQUEST (no re-scheduling)") % (existing.type, existing.uid), level=8)
+        save_object = True
+
+        # retain task status and percent-complete properties from my old copy
+        if is_task:
+            itip_event['xml'].set_status(existing.get_status())
+            itip_event['xml'].set_percentcomplete(existing.get_percentcomplete())
+
+        if policy & COND_NOTIFY:
+            send_update_notification(itip_event['xml'], receiving_user, False)
+
     # if RSVP, send an iTip REPLY
     if rsvp or scheduling_required:
         # set attendee's CN from LDAP record if yet missing
@@ -424,10 +444,6 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
             # 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_object:
         targetfolder = None
 
@@ -517,7 +533,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
             # update the organizer's copy of the object
             if update_object(existing, receiving_user):
                 if policy & COND_NOTIFY:
-                    send_reply_notification(existing, receiving_user)
+                    send_update_notification(existing, receiving_user, True)
 
                 # update all other attendee's copies
                 if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
@@ -931,7 +947,7 @@ def delete_object(existing):
     imap.imap.m.expunge()
 
 
-def send_reply_notification(object, receiving_user):
+def send_update_notification(object, receiving_user, reply=True):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
@@ -941,52 +957,56 @@ def send_reply_notification(object, receiving_user):
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
 
-    log.debug(_("Compose participation status summary for %s %r to user %r") % (
-        object.type, object.uid, receiving_user['mail']
-    ), level=8)
-
     organizer = object.get_organizer()
     orgemail = organizer.email()
     orgname = organizer.name()
 
-    auto_replies_expected = 0
-    auto_replies_received = 0
-    partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
-    for attendee in object.get_attendees():
-        parstat = attendee.get_participant_status(True)
-        if partstats.has_key(parstat):
-            partstats[parstat].append(attendee.get_displayname())
-        else:
-            partstats['PENDING'].append(attendee.get_displayname())
+    if reply:
+        log.debug(_("Compose participation status summary for %s %r to user %r") % (
+            object.type, object.uid, receiving_user['mail']
+        ), level=8)
 
-        # 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
+        auto_replies_expected = 0
+        auto_replies_received = 0
+        partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
+        for attendee in object.get_attendees():
+            parstat = attendee.get_participant_status(True)
+            if partstats.has_key(parstat):
+                partstats[parstat].append(attendee.get_displayname())
             else:
-                attendee_dn = resource_dns
-        else:
-            attendee_dn = user_dn_from_email_address(attendee.get_email())
-
-        if attendee_dn:
-            attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
-            if is_auto_reply(attendee_rec, orgemail, object.type):
-                auto_replies_expected += 1
-                if not parstat == 'NEEDS-ACTION':
-                    auto_replies_received += 1
-
-    # 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
+                partstats['PENDING'].append(attendee.get_displayname())
 
-    roundup = ''
-    for status,attendees in partstats.iteritems():
-        if len(attendees) > 0:
-            roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
+            # look-up kolabinvitationpolicy for this attendee
+            if attendee.get_cutype() == kolabformat.CutypeResource:
+                resource_dns = auth.find_resource(attendee.get_email())
+                if isinstance(resource_dns, list):
+                    attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+                else:
+                    attendee_dn = resource_dns
+            else:
+                attendee_dn = user_dn_from_email_address(attendee.get_email())
+
+            if attendee_dn:
+                attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
+                if is_auto_reply(attendee_rec, orgemail, object.type):
+                    auto_replies_expected += 1
+                    if not parstat == 'NEEDS-ACTION':
+                        auto_replies_received += 1
+
+        # 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" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
+    else:
+        # TODO: compose a diff of changes to previous version
+        roundup = "\n" + _("Minor changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
 
     # compose different notification texts for events/tasks
     if object.type == 'task':


commit b05296d7d41c7a42620d996641db9054e9da2f23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 21 10:31:16 2014 -0400

    Refactored the wallace invitationpolicy module to work for automated task iTip processing as well + add functional tests for task assignments (#3240)

diff --git a/INSTALL b/INSTALL
index b882a0a..0f83cf9 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
 * intltool
 * rpm-build
 
-* python-icalendar (version 3.8.x or higher)
+* python-icalendar (version 3.8.2 or higher)
 * python-kolabformat
 * python-kolab
 * python-nose
diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index ddcb392..1a361c1 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -1,8 +1,10 @@
 import icalendar
 import pykolab
+import traceback
 
 from pykolab.xml import to_dt
 from pykolab.xml import event_from_ical
+from pykolab.xml import todo_from_ical
 from pykolab.xml import participant_status_label
 from pykolab.translate import _
 
@@ -10,13 +12,13 @@ log = pykolab.getLogger('pykolab.wallace')
 
 
 def events_from_message(message, methods=None):
-    return objects_from_message(message, "VEVENT", methods)
+    return objects_from_message(message, ["VEVENT"], methods)
 
 def todos_from_message(message, methods=None):
-    return objects_from_message(message, "VTODO", methods)
+    return objects_from_message(message, ["VTODO"], methods)
 
 
-def objects_from_message(message, objname, methods=None):
+def objects_from_message(message, objnames, methods=None):
     """
         Obtain the iTip payload from email.message <message>
     """
@@ -60,7 +62,7 @@ def objects_from_message(message, objname, methods=None):
                 return []
 
             for c in cal.walk():
-                if c.name == objname:
+                if c.name in objnames:
                     itip = {}
 
                     if c['uid'] in seen_uids:
@@ -80,13 +82,14 @@ def objects_from_message(message, objname, methods=None):
                     # - resources (if any)
                     #
 
+                    itip['type'] = 'task' if c.name == 'VTODO' else 'event'
                     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:
+                    elif itip['type'] == 'VEVENT':
                         log.error(_("iTip event without a start"))
                         continue
 
@@ -110,17 +113,20 @@ def objects_from_message(message, objname, methods=None):
                     itip['raw'] = itip_payload
 
                     try:
-                        # TODO: distinguish event and todo here
-                        itip['xml'] = event_from_ical(c.to_ical())
+                        # distinguish event and todo here
+                        if itip['type'] == 'task':
+                            itip['xml'] = todo_from_ical(c.to_ical())
+                        else:
+                            itip['xml'] = event_from_ical(c.to_ical())
                     except Exception, e:
-                        log.error("event_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
+                        log.error("event|todo_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
                         continue
 
                     itip_objects.append(itip)
 
                     seen_uids.append(c['uid'])
 
-                # end if c.name == "VEVENT"
+                # end if c.name in objnames
 
             # end for c in cal.walk()
 
@@ -212,6 +218,8 @@ def send_reply(from_address, itip_events, response_text, subject=None):
         attendee = itip_event['xml'].get_attendee_by_email(from_address)
         participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
 
+        log.debug(_("Send iTip reply %s for %s %r") % (participant_status, itip_event['xml'].type, itip_event['xml'].uid), level=8)
+
         event_summary = itip_event['xml'].get_summary()
         message_text = response_text % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
 
@@ -226,7 +234,7 @@ def send_reply(from_address, itip_events, response_text, subject=None):
                 subject=subject
             )
         except Exception, e:
-            log.error(_("Failed to compose iTip reply message: %r") % (e))
+            log.error(_("Failed to compose iTip reply message: %r: %s") % (e, traceback.format_exc()))
             return
 
         smtp = smtplib.SMTP("localhost", 10026)  # replies go through wallace again
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index a6384e9..cdfe86a 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -12,13 +12,16 @@ participant_status_labels = {
         "TENTATIVE": N_("Tentatively Accepted"),
         "DELEGATED": N_("Delegated"),
         "COMPLETED": N_("Completed"),
-        "IN-PROCESS": N_("In Process"),
+        "IN-PROCESS": N_("Started"),
         # support integer values, too
         kolabformat.PartNeedsAction: N_("Needs Action"),
         kolabformat.PartAccepted: N_("Accepted"),
         kolabformat.PartDeclined: N_("Declined"),
         kolabformat.PartTentative: N_("Tentatively Accepted"),
         kolabformat.PartDelegated: N_("Delegated"),
+        # waiting for libkolabxml to support these (#3472)
+        #kolabformat.PartCompleted: N_("Completed"),
+        #kolabformat.PartInProcess: N_("Started"),
     }
 
 def participant_status_label(status):
@@ -38,9 +41,9 @@ class Attendee(kolabformat.Attendee):
             "DECLINED": kolabformat.PartDeclined,
             "TENTATIVE": kolabformat.PartTentative,
             "DELEGATED": kolabformat.PartDelegated,
-            # Not yet implemented
-            #"COMPLETED": ,
-            #"IN-PROCESS": ,
+            # waiting for libkolabxml to support these (#3472)
+            #"COMPLETED": kolabformat.PartCompleted,
+            #"IN-PROCESS": kolabformat.PartInProcess,
         }
 
     # See RFC 2445, 5445
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index b04b233..0d34c63 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -117,6 +117,10 @@ class Todo(Event):
     def set_percentcomplete(self, percent):
         self.event.setPercentComplete(int(percent))
 
+    def set_transparency(self, transp):
+        # empty stub
+        pass
+
     def get_due(self):
         return xmlutils.from_cdatetime(self.event.due(), True)
 
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index a4e1ebe..4e69f2f 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -189,6 +189,9 @@ class TestResourceInvitation(unittest.TestCase):
 
     @classmethod
     def setup_class(self, *args, **kw):
+        # set language to default
+        pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
         self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s")
 
         from tests.functional.purge_users import purge_users
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index bdbfc98..dffc9df 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 todo_from_message
 from pykolab.xml import participant_status_label
 from email import message_from_string
 from twisted.trial import unittest
@@ -126,6 +127,75 @@ END:VEVENT
 END:VCALENDAR
 """
 
+itip_todo = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:%(start)s
+DUE;VALUE=DATE-TIME;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:NEEDS-ACTION
+PERCENT-COMPLETE:0
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+END:VTODO
+END:VCALENDAR
+"""
+
+itip_todo_reply = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+  2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REPLY
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140821T085424Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:%(start)s
+DUE;VALUE=DATE-TIME;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:NEEDS-ACTION
+PERCENT-COMPLETE:40
+ATTENDEE;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mailto:%(mailto)s
+ORGANIZER;CN="Doe, John":mailto:%(organizer)s
+END:VTODO
+END:VCALENDAR
+"""
+
+itip_todo_cancel = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:CANCELLED
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+END:VTODO
+END:VCALENDAR
+"""
+
 mime_message = """MIME-Version: 1.0
 Content-Type: multipart/mixed;
  boundary="=_c8894dbdb8baeedacae836230e3436fd"
@@ -164,6 +234,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     @classmethod
     def setup_class(self, *args, **kw):
+        # set language to default
+        pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
         self.itip_reply_subject = _('"%(summary)s" has been %(status)s')
 
         from tests.functional.purge_users import purge_users
@@ -175,7 +248,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             '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',
+            'kolabcalendarfolder': 'user/john.doe/Calendar at example.org',
+            'kolabtasksfolder': 'user/john.doe/Tasks at example.org',
             'kolabinvitationpolicy': ['ACT_UPDATE_AND_NOTIFY','ACT_MANUAL']
         }
 
@@ -185,8 +259,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             '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']
+            'kolabcalendarfolder': 'user/jane.manager/Calendar at example.org',
+            'kolabtasksfolder': 'user/jane.manager/Tasks at example.org',
+            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','TASK_ACCEPT','ACT_UPDATE']
         }
 
         self.jack = {
@@ -195,8 +270,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             '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']
+            'kolabcalendarfolder': 'user/jack.tentative/Calendar at example.org',
+            'kolabtasksfolder': 'user/jack.tentative/Tasks at example.org',
+            'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ALL_SAVE_TO_FOLDER','ACT_UPDATE']
         }
 
         self.mark = {
@@ -205,7 +281,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             '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',
+            'kolabcalendarfolder': 'user/mark.german/Calendar at example.org',
+            'kolabtasksfolder': 'user/mark.german/Tasks at example.org',
             'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE_AND_NOTIFY']
         }
 
@@ -298,8 +375,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def send_itip_cancel(self, attendee_email, uid, summary="test", sequence=1):
-        self.send_message(itip_cancellation % {
+    def send_itip_cancel(self, attendee_email, uid, template=None, summary="test", sequence=1):
+        self.send_message((template if template is not None else itip_cancellation) % {
                 'uid': uid,
                 'mailto': attendee_email,
                 'summary': summary,
@@ -342,7 +419,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         imap = IMAP()
         imap.connect()
 
-        mailbox = imap.folder_quote(user['kolabtargetfolder'])
+        mailbox = imap.folder_quote(user['kolabcalendarfolder'])
         imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
         imap.imap.m.select(mailbox)
 
@@ -355,11 +432,45 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return event.get_uid()
 
+    def create_task_assignment(self, due=None, summary="test", sequence=0, user=None, attendees=None):
+        if due is None:
+            due = datetime.datetime.now(pytz.timezone("Europe/Berlin")) + datetime.timedelta(days=2)
+        if user is None:
+            user = self.john
+        if attendees is None:
+            attendees = [self.jane]
+
+        todo = pykolab.xml.Todo()
+        todo.set_due(due)
+        todo.set_organizer(user['mail'], user['displayname'])
+
+        for attendee in attendees:
+            todo.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+
+        todo.set_summary(summary)
+        todo.set_sequence(sequence)
+
+        imap = IMAP()
+        imap.connect()
+
+        mailbox = imap.folder_quote(user['kolabtasksfolder'])
+        imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+        imap.imap.m.select(mailbox)
+
+        result = imap.imap.m.append(
+            mailbox,
+            None,
+            None,
+            todo.to_message().as_string()
+        )
+
+        return todo.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)
+        event = self.check_user_calendar_event(user['kolabcalendarfolder'], uid)
         if event:
             if start is not None:
                 event.set_start(start)
@@ -371,7 +482,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             imap = IMAP()
             imap.connect()
 
-            mailbox = imap.folder_quote(user['kolabtargetfolder'])
+            mailbox = imap.folder_quote(user['kolabcalendarfolder'])
             imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
             imap.imap.m.select(mailbox)
 
@@ -416,6 +527,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         return found
 
     def check_user_calendar_event(self, mailbox, uid=None):
+        return self.check_user_imap_object(mailbox, uid)
+
+    def check_user_imap_object(self, mailbox, uid=None, type='event'):
         imap = IMAP()
         imap.connect()
 
@@ -429,16 +543,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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")')
+            typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.' + type + '")')
             for num in data[0].split():
                 typ, data = imap.imap.m.fetch(num, '(RFC822)')
-                event_message = message_from_string(data[0][1])
+                object_message = message_from_string(data[0][1])
 
                 # return matching UID or first event found
-                if uid and event_message['subject'] != uid:
+                if uid and object_message['subject'] != uid:
                     continue
 
-                found = event_from_message(event_message)
+                if type == 'task':
+                    found = todo_from_message(object_message)
+                else:
+                    found = event_from_message(object_message)
+
                 if found:
                     break
 
@@ -468,7 +586,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test")
 
@@ -476,7 +594,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], 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)
@@ -489,7 +607,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test2")
 
@@ -515,7 +633,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], 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)
@@ -530,7 +648,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test")
 
@@ -543,7 +661,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_start(), new_start)
         self.assertEqual(event.get_sequence(), 1)
@@ -551,7 +669,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     def test_005_invite_rescheduling_reject(self):
         self.purge_mailbox(self.john['mailbox'])
-        self.purge_mailbox(self.jack['kolabtargetfolder'])
+        self.purge_mailbox(self.jack['kolabcalendarfolder'])
 
         start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.send_itip_invitation(self.jack['mail'], start)
@@ -568,7 +686,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_start(), new_start)
         self.assertEqual(event.get_sequence(), 1)
@@ -584,7 +702,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send a reply from jane to john
@@ -592,7 +710,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # check for the updated event in john's calendar
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         attendee = event.get_attendee(self.jane['mail'])
@@ -611,7 +729,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,28, 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)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send a reply from jane to john
@@ -619,7 +737,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # check for the updated event in john's calendar
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         attendee = event.get_attendee(self.jane['mail'])
@@ -646,7 +764,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
 
         time.sleep(10)
-        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "cancelled")
         self.assertEqual(event.get_status(True), 'CANCELLED')
@@ -723,7 +841,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # verify jane's attendee status was not updated
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], 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)
@@ -735,7 +853,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send invitations to jack and jane
@@ -745,7 +863,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # 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)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # check updated event in organizer's calendar
@@ -753,12 +871,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        janes = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], 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)
+        jacks = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], 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)
 
@@ -773,7 +891,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # wait for replies to be processed and propagated
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # check updated event in organizer's calendar (jack didn't reply yet)
@@ -781,8 +899,78 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        jacks = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], 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)
 
 
+    def test_011_task_assignment_accept(self):
+        start = datetime.datetime(2014,9,10, 19,0,0)
+        uid = self.send_itip_invitation(self.jane['mail'], start, summary='work', template=itip_todo)
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'work', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+        self.assertEqual(todo.get_summary(), "work")
+
+        # send update with the same sequence: no re-scheduling
+        self.send_itip_update(self.jane['mail'], uid, start, summary='work updated', template=itip_todo, sequence=0, partstat='ACCEPTED')
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'work updated', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+        self.assertEqual(response, None)
+
+        time.sleep(10)
+        todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+        self.assertEqual(todo.get_summary(), "work updated")
+        self.assertEqual(todo.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+
+    def test_012_task_assignment_reply(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        due = datetime.datetime(2014,9,12, 14,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_task_assignment(due, user=self.john)
+
+        todo = self.check_user_imap_object(self.john['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+
+        # send a reply from jane to john
+        partstat = 'DECLINED'
+        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=due, template=itip_todo_reply, partstat=partstat)
+
+        # check for the updated task in john's tasklist
+        time.sleep(10)
+        todo = self.check_user_imap_object(self.john['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+
+        attendee = todo.get_attendee(self.jane['mail'])
+        self.assertIsInstance(attendee, pykolab.xml.Attendee)
+        self.assertEqual(attendee.get_participant_status(True), partstat)
+
+        # this should trigger an update 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(participant_status_label(partstat), notification_text)
+
+
+    def test_013_task_cancellation(self):
+        uid = self.send_itip_invitation(self.jane['mail'], summary='more work', template=itip_todo)
+
+        time.sleep(10)
+        self.send_itip_cancel(self.jane['mail'], uid, template=itip_todo_cancel, summary="cancelled")
+
+        time.sleep(10)
+        todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+        self.assertEqual(todo.get_summary(), "more work")
+        self.assertEqual(todo.get_status(True), 'CANCELLED')
+
+        # this should trigger a notification message
+        notification = self.check_message_received(_('"%s" has been cancelled') % ('more work'), self.john['mail'], mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notification, email.message.Message)
+
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index aabf676..3366950 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -117,16 +117,18 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     def test_003_get_matching_invitation_policy(self):
         user = { 'kolabinvitationpolicy': [
-            'ACT_ACCEPT:example.org',
-            'ACT_REJECT:gmail.com',
-            'ACT_MANUAL:*'
+            'TASK_REJECT:*',
+            'EVENT_ACCEPT:example.org',
+            'EVENT_REJECT:gmail.com',
+            'ALL_MANUAL:*'
         ] }
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'a at fastmail.net'), [MIP.ACT_MANUAL])
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'b at example.org'),  [MIP.ACT_ACCEPT,MIP.ACT_MANUAL])
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'c at gmail.com'),    [MIP.ACT_REJECT,MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'a at fastmail.net',   MIP.COND_TYPE_EVENT), [MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'b at example.org',    MIP.COND_TYPE_EVENT), [MIP.ACT_ACCEPT, MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'c at gmail.com',      MIP.COND_TYPE_EVENT), [MIP.ACT_REJECT, MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net', MIP.COND_TYPE_TASK),  [MIP.ACT_REJECT, MIP.ACT_MANUAL])
 
         user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] }
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net'), [MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net', MIP.COND_TYPE_ALL), [MIP.ACT_MANUAL])
 
     def test_004_write_locks(self):
         user = { 'cn': 'John Doe', 'mail': "doe at example.org" }
@@ -150,12 +152,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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 },   'user at domain.org'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none },  'user at domain.org'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all },   'user at domain.com'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond },  'user at domain.com'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'user at domain.com'))
-        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'sam at example.org'))
-        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user at domain.com'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john at example.org'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual },   'user at domain.org', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none },  'user at domain.org', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all },   'user at domain.com', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond },  'user at domain.com', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'user at domain.com', 'event'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'sam at example.org', 'event'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user at domain.com', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john at example.org', 'event'))
         
\ No newline at end of file
diff --git a/tests/unit/test-016-todo.py b/tests/unit/test-016-todo.py
index a7e9394..c6a1178 100644
--- a/tests/unit/test-016-todo.py
+++ b/tests/unit/test-016-todo.py
@@ -18,7 +18,6 @@ VERSION:2.0
 PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
  2.1.3//EN
 CALSCALE:GREGORIAN
-METHOD:REQUEST
 BEGIN:VTODO
 UID:18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0
 DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 8e77335..e753c38 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,30 +41,32 @@ 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 todo_from_message
 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 objects_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.itip import send_reply
 from pykolab.translate import _
 
 # define some contstants used in the code below
-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 + 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
+ACT_SAVE_TO_FOLDER = 32
+
+COND_IF_AVAILABLE  = 64
+COND_IF_CONFLICT   = 128
+COND_TENTATIVE     = 256
+COND_NOTIFY        = 512
+COND_TYPE_EVENT    = 1024
+COND_TYPE_TASK     = 2048
+COND_TYPE_ALL      = COND_TYPE_EVENT + COND_TYPE_TASK
+
+ACT_TENTATIVE         = ACT_ACCEPT + COND_TENTATIVE
+ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY
 
 FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
 
@@ -72,21 +74,56 @@ 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,
-    'ACT_SAVE_TO_CALENDAR':         ACT_SAVE_TO_CALENDAR
+    # policy values applying to all object types
+    'ALL_MANUAL':                     ACT_MANUAL + COND_TYPE_ALL,
+    'ALL_ACCEPT':                     ACT_ACCEPT + COND_TYPE_ALL,
+    'ALL_REJECT':                     ACT_REJECT + COND_TYPE_ALL,
+    'ALL_DELEGATE':                   ACT_DELEGATE + COND_TYPE_ALL,  # not implemented
+    'ALL_UPDATE':                     ACT_UPDATE + COND_TYPE_ALL,
+    'ALL_UPDATE_AND_NOTIFY':          ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
+    'ALL_SAVE_TO_FOLDER':             ACT_SAVE_TO_FOLDER + COND_TYPE_ALL,
+    # event related policy values
+    'EVENT_MANUAL':                   ACT_MANUAL + COND_TYPE_EVENT,
+    'EVENT_ACCEPT':                   ACT_ACCEPT + COND_TYPE_EVENT,
+    'EVENT_TENTATIVE':                ACT_TENTATIVE + COND_TYPE_EVENT,
+    'EVENT_REJECT':                   ACT_REJECT + COND_TYPE_EVENT,
+    'EVENT_DELEGATE':                 ACT_DELEGATE + COND_TYPE_EVENT,  # not implemented
+    'EVENT_UPDATE':                   ACT_UPDATE + COND_TYPE_EVENT,
+    'EVENT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY + COND_TYPE_EVENT,
+    'EVENT_ACCEPT_IF_NO_CONFLICT':    ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'EVENT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'EVENT_DELEGATE_IF_CONFLICT':     ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'EVENT_REJECT_IF_CONFLICT':       ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'EVENT_SAVE_TO_FOLDER':           ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
+    # task related policy values
+    'TASK_MANUAL':                    ACT_MANUAL + COND_TYPE_TASK,
+    'TASK_ACCEPT':                    ACT_ACCEPT + COND_TYPE_TASK,
+    'TASK_REJECT':                    ACT_REJECT + COND_TYPE_TASK,
+    'TASK_DELEGATE':                  ACT_DELEGATE + COND_TYPE_TASK,  # not implemented
+    'TASK_UPDATE':                    ACT_UPDATE + COND_TYPE_TASK,
+    'TASK_UPDATE_AND_NOTIFY':         ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK,
+    'TASK_SAVE_TO_FOLDER':            ACT_SAVE_TO_FOLDER + COND_TYPE_TASK,
+    # legacy values
+    'ACT_MANUAL':                     ACT_MANUAL + COND_TYPE_ALL,
+    'ACT_ACCEPT':                     ACT_ACCEPT + COND_TYPE_ALL,
+    'ACT_ACCEPT_IF_NO_CONFLICT':      ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'ACT_TENTATIVE':                  ACT_TENTATIVE + COND_TYPE_EVENT,
+    'ACT_TENTATIVE_IF_NO_CONFLICT':   ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'ACT_DELEGATE':                   ACT_DELEGATE + COND_TYPE_ALL,
+    'ACT_DELEGATE_IF_CONFLICT':       ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'ACT_REJECT':                     ACT_REJECT + COND_TYPE_ALL,
+    'ACT_REJECT_IF_CONFLICT':         ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'ACT_UPDATE':                     ACT_UPDATE + COND_TYPE_ALL,
+    'ACT_UPDATE_AND_NOTIFY':          ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
+    'ACT_SAVE_TO_CALENDAR':           ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
 }
 
-policy_value_map = dict([(v, k) for (k, v) in policy_name_map.iteritems()])
+policy_value_map = dict([(v &~ COND_TYPE_ALL, k) for (k, v) in policy_name_map.iteritems()])
+
+object_type_conditons = {
+    'event': COND_TYPE_EVENT,
+    'task':  COND_TYPE_TASK
+}
 
 log = pykolab.getLogger('pykolab.wallace')
 conf = pykolab.getConf()
@@ -210,17 +247,17 @@ 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 = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL'])
+        itip_events = objects_from_message(message, ['VEVENT','VTODO'], ['REQUEST', 'REPLY', 'CANCEL'])
     except Exception, e:
-        log.error(_("Failed to parse iTip events from message: %r" % (e)))
+        log.error(_("Failed to parse iTip objects 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."))
+        log.info(_("Message is not an iTip message or does not contain any (valid) iTip objects."))
 
     else:
         any_itips = True
-        log.debug(_("iTip events attached to this message contain the following information: %r") % (itip_events), level=9)
+        log.debug(_("iTip objects 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:
@@ -239,7 +276,7 @@ def execute(*args, **kw):
         log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5)
         return filepath
 
-    # we're looking at the first itip event object
+    # we're looking at the first itip object
     itip_event = itip_events[0]
 
     # for replies, the organizer is the recipient
@@ -267,7 +304,8 @@ def execute(*args, **kw):
         pykolab.translate.setUserLanguage(receiving_user['preferredlanguage'])
 
     # find user's kolabInvitationPolicy settings and the matching policy values
-    policies = get_matching_invitation_policies(receiving_user, sender_email)
+    type_condition = object_type_conditons.get(itip_event['type'], COND_TYPE_ALL)
+    policies = get_matching_invitation_policies(receiving_user, sender_email, type_condition)
 
     # select a processing function according to the iTip request method
     method_processing_map = {
@@ -326,31 +364,32 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         return MESSAGE_FORWARD
 
     # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
+    is_task = itip_event['type'] == 'task'
     nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant
     partstat = receiving_attendee.get_participant_status()
-    save_event = not nonpart or not partstat == kolabformat.PartNeedsAction
+    save_object = not nonpart or not partstat == kolabformat.PartNeedsAction
     rsvp = receiving_attendee.get_rsvp()
     scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction
     respond_with = receiving_attendee.get_participant_status(True)
     condition_fulfilled = True
 
     # find existing event in user's calendar
-    existing = find_existing_event(itip_event['uid'], receiving_user, True)
+    existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
 
     # compare sequence number to determine a (re-)scheduling request
     if existing is not None:
-        log.debug(_("Existing event: %r") % (existing), level=9)
+        log.debug(_("Existing %s: %r") % (existing.type, existing), level=9)
         scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence()
-        save_event = True
+        save_object = True
 
-    # if scheduling: check availability
+    # if scheduling: check availability (skip that for tasks)
     if scheduling_required:
-        if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
+        if not is_task and policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
             condition_fulfilled = check_availability(itip_event, receiving_user)
-        if policy & COND_IF_CONFLICT:
+        if not is_task and policy & COND_IF_CONFLICT:
             condition_fulfilled = not condition_fulfilled
 
-        log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
+        log.debug(_("Precondition for object %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
 
         respond_with = None
         if policy & ACT_ACCEPT and condition_fulfilled:
@@ -373,13 +412,13 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         # send iTip reply
         if respond_with is not None:
             receiving_attendee.set_participant_status(respond_with)
-            send_reply(recipient_email, itip_event, invitation_response_text(),
+            send_reply(recipient_email, itip_event, invitation_response_text(itip_event['type']),
                 subject=_('"%(summary)s" has been %(status)s'))
 
-        elif policy & ACT_SAVE_TO_CALENDAR:
-            # copy the invitation into the user's calendar with PARTSTAT=NEEDS-ACTION
+        elif policy & ACT_SAVE_TO_FOLDER:
+            # copy the invitation into the user's default folder with PARTSTAT=NEEDS-ACTION
             itip_event['xml'].set_attendee_participant_status(receiving_attendee, 'NEEDS-ACTION')
-            save_event = True
+            save_object = True
 
         else:
             # policy doesn't match, pass on to next one
@@ -389,17 +428,17 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
         # TODO: only update if policy & ACT_UPDATE ?
 
-    if save_event:
+    if save_object:
         targetfolder = None
 
         if existing:
             # delete old version from IMAP
             targetfolder = existing._imap_folder
-            delete_event(existing)
+            delete_object(existing)
 
         if not nonpart or existing:
             # save new copy from iTip
-            if store_event(itip_event['xml'], receiving_user, targetfolder):
+            if store_object(itip_event['xml'], receiving_user, targetfolder):
                 return MESSAGE_PROCESSED
 
     return None
@@ -426,23 +465,23 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
 
         # find existing event in user's calendar
         # sets/checks lock to avoid concurrent wallace processes trying to update the same event simultaneously
-        existing = find_existing_event(itip_event['uid'], receiving_user, True)
+        existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
 
         if existing:
             # compare sequence number to avoid outdated replies?
             if not itip_event['sequence'] == existing.get_sequence():
-                log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Forwarding to Inbox.") % (
+                log.info(_("The iTip reply sequence (%r) doesn't match the referred object version (%r). Forwarding to Inbox.") % (
                     itip_event['sequence'], existing.get_sequence()
                 ))
                 remove_write_lock(existing._lock_key)
                 return MESSAGE_FORWARD
 
-            log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
+            log.debug(_("Auto-updating %s %r on iTip REPLY") % (existing.type, existing.uid), level=8)
             try:
                 existing_attendee = existing.get_attendee(sender_email)
                 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))
+                log.error("Could not find corresponding attende in organizer's copy: %r" % (e))
 
                 # append delegated-from attendee ?
                 if len(sender_attendee.get_delegated_from()) > 0:
@@ -475,19 +514,19 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
                 except Exception, e:
                     log.error("Could not find delegated-to attendee: %r" % (e))
 
-            # update the organizer's copy of the event
-            if update_event(existing, receiving_user):
+            # update the organizer's copy of the object
+            if update_object(existing, receiving_user):
                 if policy & COND_NOTIFY:
                     send_reply_notification(existing, receiving_user)
 
                 # update all other attendee's copies
                 if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
-                    propagate_changes_to_attendees_calendars(existing)
+                    propagate_changes_to_attendees_accounts(existing)
 
                 return MESSAGE_PROCESSED
 
         else:
-            log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+            log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
             return MESSAGE_FORWARD
 
     return None
@@ -505,18 +544,21 @@ 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, True)
+        # find existing object in user's folders
+        existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
 
         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: ?
+            if update_object(existing, receiving_user):
+                # send cancellation notification
+                if policy & ACT_UPDATE_AND_NOTIFY:
+                    send_cancel_notification(existing, receiving_user)
+
                 return MESSAGE_PROCESSED
 
         else:
-            log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+            log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
             return MESSAGE_FORWARD
 
     return None
@@ -562,7 +604,7 @@ def user_dn_from_email_address(email_address):
 user_dn_from_email_address.cache = {}
 
 
-def get_matching_invitation_policies(receiving_user, sender_email):
+def get_matching_invitation_policies(receiving_user, sender_email, type_condition=COND_TYPE_ALL):
     # get user's kolabInvitationPolicy settings
     policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else []
     if policies and not isinstance(policies, list):
@@ -583,7 +625,10 @@ def get_matching_invitation_policies(receiving_user, sender_email):
         if domain == '' or domain == '*' or str(sender_email).endswith(domain):
             value = value.upper()
             if policy_name_map.has_key(value):
-                matches.append(policy_name_map[value])
+                val = policy_name_map[value]
+                # append if type condition matches
+                if val & type_condition:
+                    matches.append(val &~ COND_TYPE_ALL)
 
     # add manual as default action
     if len(matches) == 0:
@@ -594,7 +639,7 @@ def get_matching_invitation_policies(receiving_user, sender_email):
 
 def imap_proxy_auth(user_rec):
     """
-        
+        Perform IMAP login using proxy authentication with admin credentials
     """
     global imap
 
@@ -624,23 +669,23 @@ def imap_proxy_auth(user_rec):
     return True
 
 
-def list_user_calendars(user_rec):
+def list_user_folders(user_rec, type):
     """
-        Get a list of the given user's private calendar folders
+        Get a list of the given user's private calendar/tasks folders
     """
     global imap
 
     # return cached list
-    if user_rec.has_key('_calendar_folders'):
-        return user_rec['_calendar_folders'];
+    if user_rec.has_key('_imap_folders'):
+        return user_rec['_imap_folders'];
 
-    calendars = []
+    result = []
 
     if not imap_proxy_auth(user_rec):
-        return calendars
+        return result
 
     folders = imap.list_folders('*')
-    log.debug(_("List calendar folders for user %r: %r") % (user_rec['mail'], folders), level=8)
+    log.debug(_("List %r folders for user %r: %r") % (type, user_rec['mail'], folders), level=8)
 
     (ns_personal, ns_other, ns_shared) = imap.namespaces()
 
@@ -658,23 +703,23 @@ def list_user_calendars(user_rec):
         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)
+            metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith(type) \
+            or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith(type)):
+            result.append(folder)
 
-            # store default calendar folder in user record
+            # store default folder 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
+                user_rec['_default_folder'] = folder
 
     # cache with user record
-    user_rec['_calendar_folders'] = calendars
+    user_rec['_imap_folders'] = result
 
-    return calendars
+    return result
 
 
-def find_existing_event(uid, user_rec, lock=False):
+def find_existing_object(uid, type, user_rec, lock=False):
     """
-        Search user's calendar folders for the given event (by UID)
+        Search user's private folders for the given object (by UID+type)
     """
     global imap
 
@@ -685,8 +730,8 @@ def find_existing_event(uid, user_rec, lock=False):
         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)
+    for folder in list_user_folders(user_rec, type):
+        log.debug(_("Searching folder %r for %s %r") % (folder, type, uid), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
         typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
@@ -694,11 +739,15 @@ def find_existing_event(uid, user_rec, lock=False):
             typ, data = imap.imap.m.fetch(num, '(RFC822)')
 
             try:
-                event = event_from_message(message_from_string(data[0][1]))
+                if type == 'task':
+                    event = todo_from_message(message_from_string(data[0][1]))
+                else:
+                    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: %s") % (folder, num, traceback.format_exc()))
+                log.error(_("Failed to parse %s from message %s/%s: %s") % (type, folder, num, traceback.format_exc()))
                 continue
 
             if event and event.uid == uid:
@@ -723,7 +772,7 @@ def check_availability(itip_event, receiving_user):
     if itip_event.has_key('_conflicts'):
         return not itip_event['_conflicts']
 
-    for folder in list_user_calendars(receiving_user):
+    for folder in list_user_folders(receiving_user, 'event'):
         log.debug(_("Listing events from folder %r") % (folder), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
@@ -810,39 +859,40 @@ def get_lock_key(user, uid):
     return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest()
 
 
-def update_event(event, user_rec):
+def update_object(object, user_rec):
     """
-        Update the given event in IMAP (i.e. delete + append)
+        Update the given object in IMAP (i.e. delete + append)
     """
     success = False
 
-    if hasattr(event, '_imap_folder'):
-        delete_event(event)
-        success = store_event(event, user_rec, event._imap_folder)
+    if hasattr(object, '_imap_folder'):
+        delete_object(object)
+        object.set_lastmodified()  # update last-modified timestamp
+        success = store_object(object, user_rec, object._imap_folder)
 
         # remove write lock for this event
-        if hasattr(event, '_lock_key') and event._lock_key is not None:
-            remove_write_lock(event._lock_key)
+        if hasattr(object, '_lock_key') and object._lock_key is not None:
+            remove_write_lock(object._lock_key)
 
     return success
 
 
-def store_event(event, user_rec, targetfolder=None):
+def store_object(object, user_rec, targetfolder=None):
     """
-        Append the given event object to the user's default calendar
+        Append the given object to the user's default calendar/tasklist
     """
-
-    # find default calendar folder to save event to
+    
+    # find default calendar folder to save object to
     if targetfolder is None:
-        targetfolder = list_user_calendars(user_rec)[0]
-        if user_rec.has_key('_default_calendar'):
-            targetfolder = user_rec['_default_calendar']
+        targetfolder = list_user_folders(user_rec, object.type)[0]
+        if user_rec.has_key('_default_folder'):
+            targetfolder = user_rec['_default_folder']
 
     if not targetfolder:
-        log.error(_("Failed to save event: no calendar folder found for user %r") % (user_rec['mail']))
+        log.error(_("Failed to save %s: no target folder found for user %r") % (object.type, user_rec['mail']))
         return Fasle
 
-    log.debug(_("Save event %r to user calendar %r") % (event.uid, targetfolder), level=8)
+    log.debug(_("Save %s %r to user folder %r") % (object.type, object.uid, targetfolder), level=8)
 
     try:
         imap.imap.m.select(imap.folder_utf7(targetfolder))
@@ -850,29 +900,29 @@ def store_event(event, user_rec, targetfolder=None):
             imap.folder_utf7(targetfolder),
             None,
             None,
-            event.to_message(creator="Kolab Server <wallace at localhost>").as_string()
+            object.to_message(creator="Kolab Server <wallace at localhost>").as_string()
         )
         return result
 
     except Exception, e:
-        log.error(_("Failed to save event to user calendar at %r: %r") % (
-            targetfolder, e
+        log.error(_("Failed to save %s to user folder at %r: %r") % (
+            object.type, targetfolder, e
         ))
 
     return False
 
 
-def delete_event(existing):
+def delete_object(existing):
     """
-        Removes the IMAP object with the given UID from a user's calendar folder
+        Removes the IMAP object with the given UID from a user's 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
+    log.debug(_("Delete %s %r in %r: %r") % (
+        existing.type, existing.uid, targetfolder, data
     ), level=8)
 
     for num in data[0].split():
@@ -881,7 +931,7 @@ def delete_event(existing):
     imap.imap.m.expunge()
 
 
-def send_reply_notification(event, receiving_user):
+def send_reply_notification(object, receiving_user):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
@@ -891,18 +941,18 @@ def send_reply_notification(event, receiving_user):
     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']
+    log.debug(_("Compose participation status summary for %s %r to user %r") % (
+        object.type, object.uid, receiving_user['mail']
     ), level=8)
 
-    organizer = event.get_organizer()
+    organizer = object.get_organizer()
     orgemail = organizer.email()
     orgname = organizer.name()
 
     auto_replies_expected = 0
     auto_replies_received = 0
-    partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] }
-    for attendee in event.get_attendees():
+    partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
+    for attendee in object.get_attendees():
         parstat = attendee.get_participant_status(True)
         if partstats.has_key(parstat):
             partstats[parstat].append(attendee.get_displayname())
@@ -921,7 +971,7 @@ def send_reply_notification(event, receiving_user):
 
         if attendee_dn:
             attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
-            if is_auto_reply(attendee_rec, orgemail):
+            if is_auto_reply(attendee_rec, orgemail, object.type):
                 auto_replies_expected += 1
                 if not parstat == 'NEEDS-ACTION':
                     auto_replies_received += 1
@@ -938,21 +988,33 @@ def send_reply_notification(event, receiving_user):
         if len(attendees) > 0:
             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.
-        %(roundup)s
-    """ % {
-        'summary': event.get_summary(),
-        'start': event.get_start().strftime('%Y-%m-%d %H:%M %Z'),
-        'roundup': roundup
-    }
+    # compose different notification texts for events/tasks
+    if object.type == 'task':
+        message_text = """
+            The assignment for '%(summary)s' has been updated in your tasklist.
+            %(roundup)s
+        """ % {
+            'summary': object.get_summary(),
+            'roundup': roundup
+        }
+    else:
+        message_text = """
+            The event '%(summary)s' at %(start)s has been updated in your calendar.
+            %(roundup)s
+        """ % {
+            'summary': object.get_summary(),
+            'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+            'roundup': roundup
+        }
+
+    message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
 
     # 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())
+    msg['Subject'] = _('"%s" has been updated') % (object.get_summary())
     msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
 
     smtp = smtplib.SMTP("localhost", 10027)
@@ -968,10 +1030,68 @@ def send_reply_notification(event, receiving_user):
     smtp.quit()
 
 
-def is_auto_reply(user, sender_email):
+def send_cancel_notification(object, receiving_user):
+    """
+        Send a notification about event/task cancellation
+    """
+    import smtplib
+    from email.MIMEText import MIMEText
+    from email.Utils import formatdate
+
+    log.debug(_("Send cancellation notification for %s %r to user %r") % (
+        object.type, object.uid, receiving_user['mail']
+    ), level=8)
+
+    organizer = object.get_organizer()
+    orgemail = organizer.email()
+    orgname = organizer.name()
+
+    # compose different notification texts for events/tasks
+    if object.type == 'task':
+        message_text = """
+            The assignment for '%(summary)s' has been cancelled by %(organizer)s.
+            The copy in your tasklist as been marked as cancelled accordingly.
+        """ % {
+            'summary': object.get_summary(),
+            'organizer': orgname if orgname else orgemail
+        }
+    else:
+        message_text = """
+            The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.
+            The copy in your calendar as been marked as cancelled accordingly.
+        """ % {
+            'summary': object.get_summary(),
+            'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+            'organizer': orgname if orgname else orgemail
+        }
+
+    message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
+
+    # 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 cancelled') % (object.get_summary())
+    msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
+
+    smtp = smtplib.SMTP("localhost", 10027)
+
+    if conf.debuglevel > 8:
+        smtp.set_debuglevel(True)
+
+    try:
+        smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    except Exception, e:
+        log.error(_("SMTP sendmail error: %r") % (e))
+
+    smtp.quit()
+
+
+def is_auto_reply(user, sender_email, type):
     accept_available = False
     accept_conflicts = False
-    for policy in get_matching_invitation_policies(user, sender_email):
+    for policy in get_matching_invitation_policies(user, sender_email, object_type_conditons.get(type, COND_TYPE_EVENT)):
         if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE):
             if check_policy_condition(policy, True):
                 accept_available = True
@@ -983,7 +1103,7 @@ def is_auto_reply(user, sender_email):
             return True
 
         # manual action reached
-        if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR):
+        if policy & (ACT_MANUAL | ACT_SAVE_TO_FOLDER):
             return False
 
     return False
@@ -998,45 +1118,46 @@ def check_policy_condition(policy, available):
     return condition_fulfilled
 
 
-def propagate_changes_to_attendees_calendars(event):
+def propagate_changes_to_attendees_accounts(object):
     """
-        Find and update copies of this event in all attendee's calendars
+        Find and update copies of this object in all attendee's personal folders
     """
-    for attendee in event.get_attendees():
+    for attendee in object.get_attendees():
         attendee_user_dn = user_dn_from_email_address(attendee.get_email())
         if attendee_user_dn:
             attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
-            attendee_event = find_existing_event(event.uid, attendee_user, True)  # does IMAP authenticate
-            if attendee_event:
+            attendee_object = find_existing_object(object.uid, object.type, attendee_user, True)  # does IMAP authenticate
+            if attendee_object:
                 try:
-                    attendee_entry = attendee_event.get_attendee_by_email(attendee_user['mail'])
+                    attendee_entry = attendee_object.get_attendee_by_email(attendee_user['mail'])
                 except:
                     attendee_entry = None
 
-                # copy all attendees from master event (covers additions and removals)
+                # copy all attendees from master object (covers additions and removals)
                 new_attendees = kolabformat.vectorattendee();
-                for a in event.get_attendees():
+                for a in object.get_attendees():
                     # keep my own entry intact
                     if attendee_entry is not None and attendee_entry.get_email() == a.get_email():
                         new_attendees.append(attendee_entry)
                     else:
                         new_attendees.append(a)
 
-                attendee_event.event.setAttendees(new_attendees)
+                attendee_object.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)
+                success = update_object(attendee_object, attendee_user)
+                log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], object.uid, success), level=8)
 
             else:
-                log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], event.uid), level=8)
+                log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], object.uid), level=8)
 
         else:
             log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8)
 
 
-def invitation_response_text():
-    return _("""
-        %(name)s has %(status)s your invitation for %(summary)s.
+def invitation_response_text(type):
+    footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***")
 
-        *** This is an automated response sent by the Kolab Invitation system ***
-    """)
+    if type == 'task':
+        return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer
+    else:
+        return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer


commit c5978eb22295fea9a09066dc177a5d167c58fb2c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 21 07:14:10 2014 -0400

    Make sure created and last-modified dates are saved in UTC; add folder type property to groupware objects

diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
index 9a2c103..97987d9 100644
--- a/pykolab/xml/contact.py
+++ b/pykolab/xml/contact.py
@@ -1,6 +1,8 @@
 import kolabformat
 
 class Contact(kolabformat.Contact):
+    type = 'contact'
+
     def __init__(self, *args, **kw):
         kolabformat.Contact.__init__(self, *args, **kw)
 
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 34f857a..24b026e 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -43,6 +43,8 @@ def event_from_message(message):
 
 
 class Event(object):
+    type = 'event'
+
     status_map = {
             "TENTATIVE": kolabformat.StatusTentative,
             "CONFIRMED": kolabformat.StatusConfirmed,
@@ -612,9 +614,9 @@ class Event(object):
 
     def set_created(self, _datetime=None):
         if _datetime == None:
-            _datetime = datetime.datetime.now()
+            _datetime = datetime.datetime.utcnow()
 
-        self.event.setCreated(xmlutils.to_cdatetime(_datetime, False))
+        self.event.setCreated(xmlutils.to_cdatetime(_datetime, False, True))
 
     def set_description(self, description):
         self.event.setDescription(str(description))
@@ -624,7 +626,7 @@ class Event(object):
             self.event.setComment(str(comment))
 
     def set_dtstamp(self, _datetime):
-        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
+        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True))
 
     def set_end(self, _datetime):
         valid_datetime = False
@@ -771,12 +773,12 @@ class Event(object):
 
         if _datetime == None:
             valid_datetime = True
-            _datetime = datetime.datetime.now()
+            _datetime = datetime.datetime.utcnow()
 
         if not valid_datetime:
             raise InvalidEventDateError, _("Event start needs datetime.date or datetime.datetime instance")
 
-        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
+        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True))
 
     def set_location(self, location):
         self.event.setLocation(str(location))
@@ -938,7 +940,7 @@ class Event(object):
         msg['Date'] = formatdate(localtime=True)
 
         msg.add_header('X-Kolab-MIME-Version', '3.0')
-        msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.event')
+        msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.' + self.type)
 
         text = utils.multiline_message("""
                     This is a Kolab Groupware object. To view this object you
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index 28a7b4d..b04b233 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -34,6 +34,7 @@ def todo_from_message(message):
 
 # FIXME: extend a generic pykolab.xml.Xcal class instead of Event
 class Todo(Event):
+    type = 'task'
 
     def __init__(self, from_ical="", from_string=""):
         self._attendees = []
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index bcaa480..aa05e11 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -62,10 +62,18 @@ def from_cdatetime(_cdatetime, with_timezone=True):
         return datetime.datetime(year, month, day, hour, minute, second)
 
 
-def to_cdatetime(_datetime, with_timezone=True):
+def to_cdatetime(_datetime, with_timezone=True, as_utc=False):
     """
         Convert a datetime.dateime object into a kolabformat.cDateTime instance
     """
+    # convert date into UTC timezone
+    if as_utc and hasattr(_datetime, "tzinfo"):
+        if _datetime.tzinfo is not None:
+            _datetime = _datetime.astimezone(pytz.utc)
+        else:
+            datetime = _datetime.replace(tzinfo=pytz.utc)
+        with_timezone = False
+
     (
         year,
         month,
@@ -97,4 +105,7 @@ def to_cdatetime(_datetime, with_timezone=True):
         else:
             _cdatetime.setTimezone(_datetime.tzinfo.__str__())
 
+    if as_utc:
+        _cdatetime.setUTC(True)
+
     return _cdatetime


commit b87c86a62e3157de9ee17917783f74dc3b0d756c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 19 23:35:48 2014 -0400

    Add wrapper class for libkolabxml todo objects with ical import/export.
    
    ATTENTION: requires python-icalendar version 3.8 or higher!
     VTodo implemention is incomplete in older versions.

diff --git a/INSTALL b/INSTALL
index 21c764b..b882a0a 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
 * intltool
 * rpm-build
 
-* python-icalendar
+* python-icalendar (version 3.8.x or higher)
 * python-kolabformat
 * python-kolab
 * python-nose
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 3e12716..20b7e9f 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -13,6 +13,12 @@ from event import event_from_ical
 from event import event_from_string
 from event import event_from_message
 
+from todo import Todo
+from todo import TodoIntegrityError
+from todo import todo_from_ical
+from todo import todo_from_string
+from todo import todo_from_message
+
 from utils import to_dt
 
 __all__ = [
@@ -20,9 +26,14 @@ __all__ = [
         "Contact",
         "ContactReference",
         "Event",
+        "Todo",
         "RecurrenceRule",
         "event_from_ical",
         "event_from_string",
+        "event_from_message",
+        "todo_from_ical",
+        "todo_from_string",
+        "todo_from_message",
         "to_dt",
     ]
 
@@ -30,6 +41,7 @@ errors = [
         "EventIntegrityError",
         "InvalidEventDateError",
         "InvalidAttendeeParticipantStatusError",
+        "TodoIntegrityError",
     ]
 
 __all__.extend(errors)
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index c199a5a..34f857a 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -1,7 +1,5 @@
 import datetime
 import icalendar
-from icalendar import vDatetime
-from icalendar import vText
 import kolabformat
 import pytz
 import time
@@ -49,6 +47,9 @@ class Event(object):
             "TENTATIVE": kolabformat.StatusTentative,
             "CONFIRMED": kolabformat.StatusConfirmed,
             "CANCELLED": kolabformat.StatusCancelled,
+            "COMPLETD":  kolabformat.StatusCompleted,
+            "IN-PROCESS": kolabformat.StatusInProcess,
+            "NEEDS-ACTION": kolabformat.StatusNeedsAction,
         }
 
     classification_map = {
@@ -655,20 +656,14 @@ class Event(object):
         self.event.setCustomProperties(props)
 
     def set_from_ical(self, attr, value):
+        attr = attr.replace('-', '')
         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 == "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":
+        if isinstance(value, icalendar.vDDDTypes) and hasattr(value, 'dt'):
+            value = value.dt
+
+        if attr == "categories":
             self.add_category(value)
         elif attr == "class":
             self.set_classification(value)
@@ -733,9 +728,11 @@ class Event(object):
         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)
-            self.event.setDuration(duration)
+        if hasattr(value, 'dt'):
+            value = value.dt
+
+        duration = kolabformat.Duration(value.days, 0, 0, value.seconds, False)
+        self.event.setDuration(duration)
 
     def set_ical_organizer(self, organizer):
         address = str(organizer).split(':')[-1]
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
new file mode 100644
index 0000000..28a7b4d
--- /dev/null
+++ b/pykolab/xml/todo.py
@@ -0,0 +1,202 @@
+import datetime
+import kolabformat
+import icalendar
+import pytz
+
+import pykolab
+from pykolab import constants
+from pykolab.xml import Event
+from pykolab.xml import utils as xmlutils
+from pykolab.xml.event import InvalidEventDateError
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.xml_todo')
+
+def todo_from_ical(string):
+    return Todo(from_ical=string)
+
+def todo_from_string(string):
+    return Todo(from_string=string)
+
+def todo_from_message(message):
+    todo = None
+    if message.is_multipart():
+        for part in message.walk():
+            if part.get_content_type() == "application/calendar+xml":
+                payload = part.get_payload(decode=True)
+                todo = todo_from_string(payload)
+
+            # append attachment parts to Todo object
+            elif todo and part.has_key('Content-ID'):
+                todo._attachment_parts.append(part)
+
+    return todo
+
+# FIXME: extend a generic pykolab.xml.Xcal class instead of Event
+class Todo(Event):
+
+    def __init__(self, from_ical="", from_string=""):
+        self._attendees = []
+        self._categories = []
+        self._attachment_parts = []
+
+        self.properties_map.update({
+            "due": "get_due",
+            "percent-complete": "get_percentcomplete",
+            "duration": "void",
+            "end": "void"
+        })
+
+        if from_ical == "":
+            if from_string == "":
+                self.event = kolabformat.Todo()
+            else:
+                self.event = kolabformat.readTodo(from_string, False)
+                self._load_attendees()
+        else:
+            self.from_ical(from_ical)
+
+        self.uid = self.get_uid()
+
+    def from_ical(self, ical):
+        if hasattr(icalendar.Todo, 'from_ical'):
+            ical_todo = icalendar.Todo.from_ical(ical)
+        elif hasattr(icalendar.Todo, 'from_string'):
+            ical_todo = icalendar.Todo.from_string(ical)
+
+        # use the libkolab calendaring bindings to load the full iCal data
+        if ical_todo.has_key('ATTACH') or [part for part in ical_todo.walk() if part.name == 'VALARM']:
+            self._xml_from_ical(ical)
+        else:
+            self.event = kolabformat.Todo()
+
+        for attr in list(set(ical_todo.required)):
+            if ical_todo.has_key(attr):
+                self.set_from_ical(attr.lower(), ical_todo[attr])
+
+        for attr in list(set(ical_todo.singletons)):
+            if ical_todo.has_key(attr):
+                self.set_from_ical(attr.lower(), ical_todo[attr])
+
+        for attr in list(set(ical_todo.multiple)):
+            if ical_todo.has_key(attr):
+                self.set_from_ical(attr.lower(), ical_todo[attr])
+
+        # although specified by RFC 2445/5545, icalendar doesn't have this property listed
+        if ical_todo.has_key('PERCENT-COMPLETE'):
+            self.set_from_ical('percentcomplete', ical_todo['PERCENT-COMPLETE'])
+
+    def _xml_from_ical(self, ical):
+        self.event = Todo()
+        self.event.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR")
+
+    def set_ical_due(self, due):
+        self.set_due(due)
+
+    def set_due(self, _datetime):
+        valid_datetime = False
+        if isinstance(_datetime, datetime.date):
+            valid_datetime = True
+
+        if isinstance(_datetime, datetime.datetime):
+            # If no timezone information is passed on, make it UTC
+            if _datetime.tzinfo == None:
+                _datetime = _datetime.replace(tzinfo=pytz.utc)
+
+            valid_datetime = True
+
+        if not valid_datetime:
+            raise InvalidEventDateError, _("Todo due needs datetime.date or datetime.datetime instance")
+
+        self.event.setDue(xmlutils.to_cdatetime(_datetime, True))
+
+    def set_ical_percent(self, percent):
+        self.set_percentcomplete(percent)
+
+    def set_percentcomplete(self, percent):
+        self.event.setPercentComplete(int(percent))
+
+    def get_due(self):
+        return xmlutils.from_cdatetime(self.event.due(), True)
+
+    def get_ical_due(self):
+        dt = self.get_due()
+        if dt:
+            return icalendar.vDatetime(dt)
+        return None
+
+    def get_percentcomplete(self):
+        return self.event.percentComplete()
+
+    def get_duration(self):
+        return None
+
+    def as_string_itip(self, method="REQUEST"):
+        cal = icalendar.Calendar()
+        cal.add(
+            'prodid',
+            '-//pykolab-%s-%s//kolab.org//' % (
+                constants.__version__,
+                constants.__release__
+            )
+        )
+
+        cal.add('version', '2.0')
+        cal.add('calscale', 'GREGORIAN')
+        cal.add('method', method)
+
+        ical_todo = icalendar.Todo()
+
+        singletons = list(set(ical_todo.singletons))
+        singletons.extend(['PERCENT-COMPLETE'])
+        for attr in singletons:
+            ical_getter = 'get_ical_%s' % (attr.lower())
+            default_getter = 'get_%s' % (attr.lower())
+            retval = None
+            if hasattr(self, ical_getter):
+                retval = getattr(self, ical_getter)()
+                if not retval == None and not retval == "":
+                    ical_todo.add(attr.lower(), retval)
+            elif hasattr(self, default_getter):
+                retval = getattr(self, default_getter)()
+                if not retval == None and not retval == "":
+                    ical_todo.add(attr.lower(), retval, encode=0)
+
+        for attr in list(set(ical_todo.multiple)):
+            ical_getter = 'get_ical_%s' % (attr.lower())
+            default_getter = 'get_%s' % (attr.lower())
+            retval = None
+            if hasattr(self, ical_getter):
+                retval = getattr(self, ical_getter)()
+            elif hasattr(self, default_getter):
+                retval = getattr(self, default_getter)()
+
+            if isinstance(retval, list) and not len(retval) == 0:
+                for _retval in retval:
+                    ical_todo.add(attr.lower(), _retval, encode=0)
+
+        # copy custom properties to iCal
+        for cs in self.event.customProperties():
+            ical_todo.add(cs.identifier, cs.value)
+
+        cal.add_component(ical_todo)
+
+        if hasattr(cal, 'to_ical'):
+            return cal.to_ical()
+        elif hasattr(cal, 'as_string'):
+            return cal.as_string()
+
+    def __str__(self):
+        xml = kolabformat.writeTodo(self.event)
+
+        error = kolabformat.error()
+
+        if error == None or not error:
+            return xml
+        else:
+            raise TodoIntegrityError, kolabformat.errorMessage()
+
+
+class TodoIntegrityError(Exception):
+    def __init__(self, message):
+        Exception.__init__(self, message)
diff --git a/tests/unit/test-016-todo.py b/tests/unit/test-016-todo.py
new file mode 100644
index 0000000..a7e9394
--- /dev/null
+++ b/tests/unit/test-016-todo.py
@@ -0,0 +1,240 @@
+import datetime
+import pytz
+import sys
+import unittest
+import kolabformat
+import icalendar
+
+from pykolab.xml import Attendee
+from pykolab.xml import Todo
+from pykolab.xml import TodoIntegrityError
+from pykolab.xml import todo_from_ical
+from pykolab.xml import todo_from_string
+from pykolab.xml import todo_from_message
+
+ical_todo = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTODO
+UID:18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+LAST-MODIFIED;VALUE=DATE-TIME:20140820T101333Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/London:20140818T180000
+DUE;VALUE=DATE-TIME;TZID=Europe/London:20140822T133000
+SUMMARY:Sample Task assignment
+DESCRIPTION:Summary: Sample Task assignment\\nDue Date: 08/11/14\\nDue Time:
+ \\n13:30 AM
+SEQUENCE:3
+CATEGORIES:iTip
+PRIORITY:1
+STATUS:IN-PROCESS
+PERCENT-COMPLETE:20
+ATTENDEE;CN="Doe, John";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=
+ INDIVIDUAL;RSVP=TRUE:mailto:john.doe at example.org
+ORGANIZER;CN=Thomas:mailto:thomas.bruederli at example.org
+END:VTODO
+END:VCALENDAR
+"""
+
+xml_todo = """
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+  <vcalendar>
+    <properties>
+      <prodid>
+        <text>Roundcube-libkolab-1.1 Libkolabxml-1.1</text>
+      </prodid>
+      <version>
+        <text>2.0</text>
+      </version>
+      <x-kolab-version>
+        <text>3.1.0</text>
+      </x-kolab-version>
+    </properties>
+    <components>
+      <vtodo>
+        <properties>
+          <uid>
+            <text>18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0</text>
+          </uid>
+          <created>
+            <date-time>2014-07-31T10:07:04Z</date-time>
+          </created>
+          <dtstamp>
+            <date-time>2014-08-20T10:13:33Z</date-time>
+          </dtstamp>
+          <sequence>
+            <integer>3</integer>
+          </sequence>
+          <class>
+            <text>PUBLIC</text>
+          </class>
+          <categories>
+            <text>iTip</text>
+          </categories>
+          <dtstart>
+            <parameters>
+              <tzid><text>/kolab.org/Europe/Berlin</text></tzid>
+            </parameters>
+            <date-time>2014-08-18T18:00:00</date-time>
+          </dtstart>
+          <due>
+            <parameters>
+              <tzid><text>/kolab.org/Europe/Berlin</text></tzid>
+            </parameters>
+            <date-time>2014-08-22T13:30:00</date-time>
+          </due>
+          <summary>
+            <text>Sample Task assignment</text>
+          </summary>
+          <description>
+            <text>Summary: Sample Task assignment
+Due Date: 08/11/14
+Due Time: 13:30 AM</text>
+          </description>
+          <priority>
+            <integer>1</integer>
+          </priority>
+          <status>
+            <text>IN-PROCESS</text>
+          </status>
+          <percent-complete>
+            <integer>20</integer>
+          </percent-complete>
+          <organizer>
+            <parameters>
+              <cn><text>Thomas</text></cn>
+            </parameters>
+            <cal-address>mailto:%3Cthomas%40example.org%3E</cal-address>
+          </organizer>
+          <attendee>
+            <parameters>
+              <cn><text>Doe, John</text></cn>
+              <partstat><text>NEEDS-ACTION</text></partstat>
+              <role><text>REQ-PARTICIPANT</text></role>
+              <rsvp><boolean>true</boolean></rsvp>
+            </parameters>
+            <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
+          </attendee>
+        </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>
+        </components>
+      </vtodo>
+    </components>
+  </vcalendar>
+</icalendar>
+"""
+
+class TestTodoXML(unittest.TestCase):
+    todo = Todo()
+
+    def assertIsInstance(self, _value, _type):
+        if hasattr(unittest.TestCase, 'assertIsInstance'):
+            return unittest.TestCase.assertIsInstance(self, _value, _type)
+        else:
+            if (type(_value)) == _type:
+                return True
+            else:
+                raise AssertionError, "%s != %s" % (type(_value), _type)
+
+    def test_001_minimal(self):
+        self.todo.set_summary("test")
+        self.assertEqual("test", self.todo.get_summary())
+        self.assertIsInstance(self.todo.__str__(), str)
+
+    def test_002_full(self):
+        pass
+
+    def test_010_load_from_xml(self):
+        todo = todo_from_string(xml_todo)
+        self.assertEqual(todo.uid, '18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0')
+        self.assertEqual(todo.get_sequence(), 3)
+        self.assertIsInstance(todo.get_due(), datetime.datetime)
+        self.assertEqual(str(todo.get_due()), "2014-08-22 13:30:00+01:00")
+        self.assertEqual(str(todo.get_start()), "2014-08-18 18:00:00+01:00")
+        self.assertEqual(todo.get_categories(), ['iTip'])
+        self.assertEqual(todo.get_attendee_by_email("john at example.org").get_participant_status(), kolabformat.PartNeedsAction)
+        self.assertIsInstance(todo.get_organizer(), kolabformat.ContactReference)
+        self.assertEqual(todo.get_organizer().name(), "Thomas")
+        self.assertEqual(todo.get_status(True), "IN-PROCESS")
+
+
+    def test_020_load_from_ical(self):
+        ical_str = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1.0//Sabre//Sabre VObject
+  2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+        """ + ical_todo + "END:VCALENDAR"
+
+        ical = icalendar.Calendar.from_ical(ical_str)
+        vtodo = ical.walk('VTODO')[0]
+        #print vtodo
+        todo = todo_from_ical(ical.walk('VTODO')[0].to_ical())
+        self.assertEqual(todo.get_summary(), "Sample Task assignment")
+        self.assertIsInstance(todo.get_start(), datetime.datetime)
+        self.assertEqual(todo.get_percentcomplete(), 20)
+        #print str(todo)
+
+    def test_021_as_string_itip(self):
+        self.todo.set_summary("test")
+        self.todo.set_start(datetime.datetime(2014, 9, 20, 11, 00, 00, tzinfo=pytz.timezone("Europe/London")))
+        self.todo.set_due(datetime.datetime(2014, 9, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London")))
+        self.todo.set_sequence(3)
+        self.todo.add_custom_property('X-Custom', 'check')
+
+        # render iCal and parse again using the icalendar lib
+        ical = icalendar.Calendar.from_ical(self.todo.as_string_itip())
+        vtodo = ical.walk('VTODO')[0]
+
+        self.assertEqual(vtodo['uid'], self.todo.get_uid())
+        self.assertEqual(vtodo['summary'], "test")
+        self.assertEqual(vtodo['sequence'], 3)
+        self.assertEqual(vtodo['X-CUSTOM'], "check")
+        self.assertIsInstance(vtodo['due'].dt, datetime.datetime)
+        self.assertIsInstance(vtodo['dtstamp'].dt, datetime.datetime)
+
+
+    def test_030_to_dict(self):
+        data = todo_from_string(xml_todo).to_dict()
+
+        self.assertIsInstance(data, dict)
+        self.assertIsInstance(data['start'], datetime.datetime)
+        self.assertIsInstance(data['due'], datetime.datetime)
+        self.assertEqual(data['uid'], '18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0')
+        self.assertEqual(data['summary'], 'Sample Task assignment')
+        self.assertEqual(data['description'], "Summary: Sample Task assignment\nDue Date: 08/11/14\nDue Time: 13:30 AM")
+        self.assertEqual(data['priority'], 1)
+        self.assertEqual(data['sequence'], 3)
+        self.assertEqual(data['status'], 'IN-PROCESS')
+
+        self.assertIsInstance(data['alarm'], list)
+        self.assertEqual(len(data['alarm']), 1)
+        self.assertEqual(data['alarm'][0]['action'], 'DISPLAY')
+
+
+if __name__ == '__main__':
+    unittest.main()
\ No newline at end of file


commit 50ecd9edf92d3d50492f23408e009c900a63d882
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 19 23:02:17 2014 -0400

    Translate UTC and GMT timezones into the according isUTC flag

diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index 2fddb24..bcaa480 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -92,6 +92,9 @@ def to_cdatetime(_datetime, with_timezone=True):
         _cdatetime = kolabformat.cDateTime(year, month, day)
 
     if with_timezone and hasattr(_datetime, "tzinfo"):
-        _cdatetime.setTimezone(_datetime.tzinfo.__str__())
+        if _datetime.tzinfo.__str__() in ['UTC','GMT']:
+            _cdatetime.setUTC(True)
+        else:
+            _cdatetime.setTimezone(_datetime.tzinfo.__str__())
 
     return _cdatetime


commit 57a48ed5e5fed38b4bbbb088fc9425a4b407c0b0
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 20 12:47:20 2014 +0200

    Use more secure urls by default

diff --git a/share/templates/roundcubemail/config.inc.php.tpl b/share/templates/roundcubemail/config.inc.php.tpl
index eb3f7ec..920423e 100644
--- a/share/templates/roundcubemail/config.inc.php.tpl
+++ b/share/templates/roundcubemail/config.inc.php.tpl
@@ -6,6 +6,8 @@
     \$config['session_domain'] = '';
     \$config['des_key'] = "$des_key";
     \$config['username_domain'] = '$primary_domain';
+    \$config['use_secure_urls'] = true;
+    \$config['assets_path'] = '/roundcubemail/assets/';
 
     \$config['mail_domain'] = '';
 


commit 172545ebeba5c73cbb502fcae859b27df7aafed2
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 20 12:20:05 2014 +0200

    Set the URL to files to '/chwala/' (#2436)

diff --git a/share/templates/roundcubemail/kolab_files.inc.php.tpl b/share/templates/roundcubemail/kolab_files.inc.php.tpl
index 1c5fced..bcdaccc 100644
--- a/share/templates/roundcubemail/kolab_files.inc.php.tpl
+++ b/share/templates/roundcubemail/kolab_files.inc.php.tpl
@@ -1,7 +1,7 @@
 <?php
 
 // URL of kolab-chwala installation
-\$config['kolab_files_url'] = 'http://' . \$_SERVER['HTTP_HOST'] . '/chwala/';
+\$config['kolab_files_url'] = '/chwala/';
 
 // List of files list columns. Available are: name, size, mtime, type
 \$config['kolab_files_list_cols'] = array('name', 'mtime', 'size');


commit cd3c4acd667c7cfe4366e3a044fa2126d8d7c079
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 20 12:19:40 2014 +0200

    Add kolab_tags plugin to the default configuration

diff --git a/share/templates/roundcubemail/config.inc.php.tpl b/share/templates/roundcubemail/config.inc.php.tpl
index bf61c2f..eb3f7ec 100644
--- a/share/templates/roundcubemail/config.inc.php.tpl
+++ b/share/templates/roundcubemail/config.inc.php.tpl
@@ -57,6 +57,7 @@
             'kolab_files',
             'kolab_folders',
             'kolab_notes',
+            'kolab_tags',
             'libkolab',
             'libcalendaring',
             'managesieve',


commit 2a97627541df44ffdb4b381482fab9cad72d209b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 23:24:37 2014 +0200

    Fix syntax errors

diff --git a/pykolab/setup/setup_syncroton.py b/pykolab/setup/setup_syncroton.py
index e0477b1..ca99bd5 100644
--- a/pykolab/setup/setup_syncroton.py
+++ b/pykolab/setup/setup_syncroton.py
@@ -52,9 +52,9 @@ def execute(*args, **kw):
                                 schema_files.append(schema_filepath)
                                 break
 
-                if len(schema_files) > 0
+                if len(schema_files) > 0:
                     break
-        if len(schema_files) > 0
+        if len(schema_files) > 0:
             break
 
     if not os.path.isfile('/tmp/kolab-setup-my.cnf'):


commit 5a9852f6358f6f37f1b13545ecd366880de492bd
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 22:53:32 2014 +0200

    Bump version

diff --git a/configure.ac b/configure.ac
index a783e77..0aa71ff 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,4 +1,4 @@
-AC_INIT([pykolab], 0.7.0)
+AC_INIT([pykolab], 0.7.1)
 AC_SUBST([RELEASE], 1)
 
 AC_CONFIG_SRCDIR(pykolab/constants.py.in)


commit 1da7b81cebf9bb2fd9485e5af3f59ede9a31b4c0
Author: Timotheus Pokorra (TBits.net) <tp at tbits.net>
Date:   Tue Aug 19 15:11:29 2014 +0200

    Bug #2491: sleep time until new domains are created should be optionally set in configuration file

diff --git a/conf/kolab.conf b/conf/kolab.conf
index 80d1f53..654c38e 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -22,6 +22,10 @@ default_locale = en_US
 ; deployments that lack persistent search and syncrepl ldap controls.
 sync_interval = 300
 
+; Synchronization interval for domains - describes the number of seconds
+; to wait in between polls for new and deleted domain name spaces.
+domain_sync_interval = 600
+
 ; The policy to use when originally composing the uid attribute value.
 ; Normally '%(surname)s.lower()', the transliterated value of the 'sn',
 ; in all lower-case.
diff --git a/kolabd/__init__.py b/kolabd/__init__.py
index 54905f6..92a929c 100644
--- a/kolabd/__init__.py
+++ b/kolabd/__init__.py
@@ -269,7 +269,11 @@ class KolabDaemon(object):
                     added_domains.append(domain)
 
             if len(removed_domains) == 0 and len(added_domains) == 0:
-                time.sleep(600)
+                try:
+                    sleep_between_domain_operations_in_seconds = (float)(conf.get('kolab', 'domain_sync_interval'))
+                    time.sleep(sleep_between_domain_operations_in_seconds)
+                except ValueError:
+                    time.sleep(600)
 
             log.debug(
                     _("added domains: %r, removed domains: %r") % (
diff --git a/pykolab/conf/defaults.py b/pykolab/conf/defaults.py
index 56abe6c..06e5372 100644
--- a/pykolab/conf/defaults.py
+++ b/pykolab/conf/defaults.py
@@ -33,5 +33,8 @@ class Defaults(object):
         self.mail_attributes = ['mail', 'alias']
         self.mailserver_attribute = 'mailhost'
 
+        # when you want a new domain to be added in a short time, you should reduce this value to 10 seconds
+        self.kolab_domain_sync_interval = 600
+
         self.kolab_default_locale = 'en_US'
         self.ldap_unique_attribute = 'nsuniqueid'
\ No newline at end of file


commit ee82a914da00c1f0133240abc4767ac01f4c27ea
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 14:02:30 2014 +0200

    Bug #3156 - wrong sender_login_maps

diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
index 7898599..88c6f6c 100644
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@ -260,7 +260,7 @@ result_format = shared+%%s
             "smtpd_tls_security_level": "may",
             "smtp_tls_security_level": "may",
             "smtpd_sasl_auth_enable": "yes",
-            "smtpd_sender_login_maps": "$relay_recipient_maps",
+            "smtpd_sender_login_maps": "$local_recipient_maps",
             "smtpd_sender_restrictions": "permit_mynetworks, reject_sender_login_mismatch",
             "smtpd_recipient_restrictions": "permit_mynetworks, reject_unauth_pipelining, reject_rbl_client zen.spamhaus.org, reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/recipient_policy_incoming, permit",
             "smtpd_sender_restrictions": "permit_mynetworks, check_policy_service unix:private/sender_policy_incoming",


commit ff5031a3d8f1b30857628b670da1758d345be530
Author: Aeneas Jaißle <aj at ajaissle.de>
Date:   Tue Aug 19 13:52:46 2014 +0200

    Fix #3391: setup-kolab syncroton fails on openSUSE. The setup script tries an os.walk to find msql.initial.sql, but has a 'break' stopping the walk from going one (needed) level deeper.

diff --git a/pykolab/setup/setup_syncroton.py b/pykolab/setup/setup_syncroton.py
index 2f401bd..e0477b1 100644
--- a/pykolab/setup/setup_syncroton.py
+++ b/pykolab/setup/setup_syncroton.py
@@ -44,15 +44,18 @@ def execute(*args, **kw):
     for root, directories, filenames in os.walk('/usr/share/doc/'):
         for directory in directories:
             if directory.startswith("kolab-syncroton"):
-                for root, directories, filenames in os.walk(os.path.join('/usr/share/doc/', directory)):
+                for root, directories, filenames in os.walk(os.path.join(root, directory)):
                     for filename in filenames:
                         if filename.startswith('mysql.initial') and filename.endswith('.sql'):
                             schema_filepath = os.path.join(root,filename)
                             if not schema_filepath in schema_files:
                                 schema_files.append(schema_filepath)
+                                break
 
-                break
-        break
+                if len(schema_files) > 0
+                    break
+        if len(schema_files) > 0
+            break
 
     if not os.path.isfile('/tmp/kolab-setup-my.cnf'):
         utils.multiline_message(


commit 36ef0b35a936cc49e490d132d88922968c1eb471
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Aug 19 11:57:34 2014 -0400

    Set read ACLs for admin user before accessing resource calendars (#3428)

diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 0f05993..a4e1ebe 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -323,7 +323,8 @@ class TestResourceInvitation(unittest.TestCase):
         imap = IMAP()
         imap.connect()
 
-        imap.imap.m.select(u'"'+mailbox+'"')
+        imap.set_acl(mailbox, "cyrus-admin", "lrs")
+        imap.imap.m.select(imap.folder_quote(mailbox))
 
         found = None
         retries = 10
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index c41413a..aa3c473 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -533,6 +533,9 @@ def read_resource_calendar(resource_rec, itip_events):
         level=9
     )
 
+    # set read ACLs for admin user
+    imap.set_acl(mailbox, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrs")
+
     # might raise an exception, let that bubble
     imap.imap.m.select(imap.folder_quote(mailbox))
     typ, data = imap.imap.m.search(None, 'ALL')
@@ -686,7 +689,7 @@ def save_resource_event(itip_event, resource, replace=False):
         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")
+            imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
 
         result = imap.imap.m.append(
             targetfolder,
@@ -709,7 +712,7 @@ def delete_resource_event(uid, resource):
         Removes the IMAP object with the given UID from a resource's calendar folder
     """
     targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
-    imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+    imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
     imap.imap.m.select(targetfolder)
 
     typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % uid)


commit c8560954615b94e21b7605a1e095481c6a022721
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Aug 15 18:08:36 2014 -0400

    Catch failures on base64 decoding event UIDs from owner confirmation replies (#3423)

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 55d7472..c41413a 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -205,10 +205,13 @@ 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):
-                (prefix, host) = recipient.split('@')
-                (local, uid) = prefix.split('+')
-                reference_uid = base64.b64decode(uid, '-/')
-                recipient = local + '@' + host
+                try:
+                    (prefix, host) = recipient.split('@')
+                    (local, uid) = prefix.split('+')
+                    reference_uid = base64.b64decode(uid, '-/')
+                    recipient = local + '@' + host
+                except:
+                    continue
 
             if not len(resource_record_from_email_address(recipient)) == 0:
                 resource_recipient = recipient


commit adf90b45b28b458931ff69fb77caebe3c868d84b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 13:46:41 2014 +0200

    Work around fsockopen() taking OpenSSL's default cert validation routines (#3332)

diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py
index b255bc3..83baf6e 100644
--- a/pykolab/setup/setup_freebusy.py
+++ b/pykolab/setup/setup_freebusy.py
@@ -78,6 +78,10 @@ def execute(*args, **kw):
     if scheme == None or scheme == "":
         scheme = 'imaps'
 
+    if scheme == "imaps" and port == 993:
+        scheme = "imap"
+        port = 143
+
     resources_imap_uri = '%s://%s:%s@%s:%s/%%kolabtargetfolder?acl=lrs' % (scheme, admin_login, admin_password, hostname, port)
     users_imap_uri = '%s://%%s:%s@%s:%s/?proxy_auth=%s' % (scheme, admin_password, hostname, port, admin_login)
 


commit fff8fd1189e52eab6e42cda1885cc782969d6a61
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 13:39:15 2014 +0200

    Add kolab_smtp_access_policy/cache_uri setting to set

diff --git a/pykolab/setup/setup_mysql.py b/pykolab/setup/setup_mysql.py
index 5f8ab4a..fb1b102 100644
--- a/pykolab/setup/setup_mysql.py
+++ b/pykolab/setup/setup_mysql.py
@@ -161,6 +161,7 @@ password='%s'
         p2.communicate()
 
         conf.command_set('kolab_wap', 'sql_uri', 'mysql://kolab:%s@localhost/kolab' % (mysql_kolab_password))
+        conf.command_set('kolab_smtp_access_policy', 'cache_uri', 'mysql://kolab:%s@localhost/kolab' % (mysql_kolab_password))
     else:
         log.warning(_("Could not find the MySQL Kolab schema file"))
 


commit decbb7d37719074d1c63dd26fd1120c9646e7549
Author: Daniel Hoffend <dh at dotlan.net>
Date:   Tue Aug 19 13:28:43 2014 +0200

    fix splitting of imap acl/aci from ldap entry (#3351)

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 23b80e8..b9d0749 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1235,7 +1235,7 @@ class LDAP(pykolab.base.Base):
 
             for acl_entry in entry[folderacl_entry_attribute]:
                 acl_access = acl_entry.split()[-1]
-                aci_subject = ' '.join(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)
 
@@ -1260,6 +1260,7 @@ class LDAP(pykolab.base.Base):
 
         if not self.imap.shared_folder_exists(folder_path):
             self.imap.shared_folder_create(folder_path, server)
+            self.imap.set_acl(folder_path, 'anyone', '')
 
         if entry.has_key('kolabfoldertype') and \
                 not entry['kolabfoldertype'] == None:
@@ -1275,8 +1276,6 @@ class LDAP(pykolab.base.Base):
             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:
@@ -1617,7 +1616,7 @@ class LDAP(pykolab.base.Base):
 
             for acl_entry in entry[folderacl_entry_attribute]:
                 acl_access = acl_entry.split()[-1]
-                aci_subject = ' '.join(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)
 
@@ -1642,6 +1641,7 @@ class LDAP(pykolab.base.Base):
 
         if not self.imap.shared_folder_exists(folder_path):
             self.imap.shared_folder_create(folder_path, server)
+            self.imap.set_acl(folder_path, 'anyone', '')
 
         if entry.has_key('kolabfoldertype') and \
                 not entry['kolabfoldertype'] == None:
@@ -1650,8 +1650,6 @@ class LDAP(pykolab.base.Base):
                     folder_path,
                     entry['kolabfoldertype']
                 )
-        else:
-            self.imap.set_acl(folder_path, 'anyone', '')
 
         if entry.has_key('kolabfolderaclentry') and \
                 not entry['kolabfolderaclentry'] == None:
@@ -1659,8 +1657,6 @@ class LDAP(pykolab.base.Base):
             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:


commit ef398699e6a041e718dfec2d67702edd45135e64
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 13:22:55 2014 +0200

    Drop patches from packaging (#3374)

diff --git a/pykolab/setup/setup_kolabd.py b/pykolab/setup/setup_kolabd.py
index 8b8c50e..7c7982d 100644
--- a/pykolab/setup/setup_kolabd.py
+++ b/pykolab/setup/setup_kolabd.py
@@ -27,6 +27,7 @@ import pykolab
 from pykolab import utils
 from pykolab.constants import *
 from pykolab.translate import _
+from augeas import Augeas
 
 log = pykolab.getLogger('pykolab.setup')
 conf = pykolab.getConf()
@@ -62,6 +63,14 @@ def execute(*args, **kw):
             conf.cfg_parser.write(fp)
             fp.close()
 
+    if os.path.isfile('/etc/default/kolab-server'):
+        myaugeas = Augeas()
+        setting = os.path.join('/files/etc/default/kolab-server','START')
+        if not myaugeas.get(setting) == 'yes':
+            myaugeas.set(setting,'yes')
+            myaugeas.save()
+        myaugeas.close()
+
     if os.path.isfile('/bin/systemctl'):
         subprocess.call(['/bin/systemctl', 'restart', 'kolabd.service'])
     elif os.path.isfile('/sbin/service'):
diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
index c3ab0e3..7898599 100644
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@ -420,6 +420,14 @@ result_format = shared+%%s
             myaugeas.save()
         myaugeas.close()
 
+    if os.path.isfile('/etc/default/wallace'):
+        myaugeas = Augeas()
+        setting = os.path.join('/files/etc/default/wallace','START')
+        if not myaugeas.get(setting) == 'yes':
+            myaugeas.set(setting,'yes')
+            myaugeas.save()
+        myaugeas.close()
+
     if os.path.isfile('/bin/systemctl'):
         subprocess.call(['systemctl', 'restart', 'postfix.service'])
         subprocess.call(['systemctl', 'restart', 'amavisd.service'])


commit 27f4d069f721b254c5db7249bd792d0fec52fc40
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 13:18:45 2014 +0200

    Fix #3350, _acl referenced before assignment

diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index b3af455..7ee19a6 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -319,6 +319,8 @@ class IMAP(object):
         """
             Set an ACL entry on a folder.
         """
+        _acl = []
+
         short_rights = {
                 'all': 'lrsedntxakcpiw',
                 'append': 'wip',


commit a46c3d3a34c2cf9264566c29a429bd8a1d3aec6c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 13:16:26 2014 +0200

    Fix #3349 by testing if a log file already exists

diff --git a/pykolab/logger.py b/pykolab/logger.py
index 8e92259..cce43f5 100644
--- a/pykolab/logger.py
+++ b/pykolab/logger.py
@@ -162,24 +162,26 @@ class Logger(logging.Logger):
 
                         sys.exit(1)
 
-                try:
-                    os.chown(
-                            self.logfile,
-                            user_uid,
-                            group_gid
-                        )
-                    os.chmod(self.logfile, 0660)
-                except Exception, errmsg:
-                    self.error(_("Could not change permissions on %s: %r") % (self.logfile, errmsg))
-                    if self.debuglevel > 8:
-                        import traceback
-                        traceback.print_exc()
+                if os.path.isfile(self.logfile):
+                    try:
+                        os.chown(
+                                self.logfile,
+                                user_uid,
+                                group_gid
+                            )
+                        os.chmod(self.logfile, 0660)
+                    except Exception, errmsg:
+                        self.error(_("Could not change permissions on %s: %r") % (self.logfile, errmsg))
+                        if self.debuglevel > 8:
+                            import traceback
+                            traceback.print_exc()
 
         except Exception, errmsg:
-            self.error(_("Could not change permissions on %s: %r") % (self.logfile, errmsg))
-            if self.debuglevel > 8:
-                import traceback
-                traceback.print_exc()
+            if os.path.isfile(self.logfile):
+                self.error(_("Could not change permissions on %s: %r") % (self.logfile, errmsg))
+                if self.debuglevel > 8:
+                    import traceback
+                    traceback.print_exc()
 
         # Make sure the log file exists
         try:


commit af9c30d69a2e0f6e428d239359c4bb4be7ab8595
Author: Daniel Hoffend <dh at dotlan.net>
Date:   Tue Aug 19 13:12:50 2014 +0200

    virtualaliasmaps only for sharedfolders of kolabFolderType=mail (#3311)

diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
index 166b402..c3ab0e3 100644
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@ -224,7 +224,7 @@ domain = ldap:/etc/postfix/ldap/mydestination.cf
 bind_dn = %(service_bind_dn)s
 bind_pw = %(service_bind_pw)s
 
-query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabsharedfolder))
+query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabsharedfolder)(kolabFolderType=mail))
 result_attribute = kolabtargetfolder
 result_format = shared+%%s
 """ % {


commit f179b42eb66d1f436f63c118f46d26eed33dc9ca
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 13:06:53 2014 +0200

    %mail -> %s

diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py
index dfabab5..b255bc3 100644
--- a/pykolab/setup/setup_freebusy.py
+++ b/pykolab/setup/setup_freebusy.py
@@ -101,7 +101,7 @@ def execute(*args, **kw):
                     'attributes': 'mail',
                     'lc_attributes': 'mail',
                     'fbsource': users_imap_uri,
-                    'cacheto': '/var/cache/kolab-freebusy/%mail.ifb',
+                    'cacheto': '/var/cache/kolab-freebusy/%s.ifb',
                     'expires': '15m',
                     'loglevel': 300,
                 },
@@ -114,7 +114,7 @@ def execute(*args, **kw):
                     'attributes': 'mail, kolabtargetfolder',
                     'filter': '(&(objectClass=kolabsharedfolder)(kolabfoldertype=event)(mail=%s))',
                     'fbsource': resources_imap_uri,
-                    'cacheto': '/var/cache/kolab-freebusy/%mail.ifb',
+                    'cacheto': '/var/cache/kolab-freebusy/%s.ifb',
                     'expires': '15m',
                     'loglevel': 300,
                 },


commit 6540816847dab0c14a42644f08de063520a74966
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 12:29:11 2014 +0200

    Add default settings for group ACLs

diff --git a/share/templates/roundcubemail/acl.inc.php.tpl b/share/templates/roundcubemail/acl.inc.php.tpl
index ca1bae5..57911ec 100644
--- a/share/templates/roundcubemail/acl.inc.php.tpl
+++ b/share/templates/roundcubemail/acl.inc.php.tpl
@@ -4,6 +4,9 @@
     \$config['acl_users_field'] = 'mail';
     \$config['acl_users_filter'] = 'objectClass=kolabInetOrgPerson';
 
+    \$config['acl_groups'] = true;
+    \$config['acl_group_prefix'] = 'group:';
+
     if (file_exists(RCUBE_CONFIG_DIR . '/' . \$_SERVER["HTTP_HOST"] . '/' . basename(__FILE__))) {
         include_once(RCUBE_CONFIG_DIR . '/' . \$_SERVER["HTTP_HOST"] . '/' . basename(__FILE__));
     }


commit 129472ff828b3d1789c60eaecd2b90acdeb9baf5
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 11:55:26 2014 +0200

    Fix #3418 and add new default folders

diff --git a/share/templates/roundcubemail/kolab_folders.inc.php.tpl b/share/templates/roundcubemail/kolab_folders.inc.php.tpl
index 93f6eec..4be282e 100644
--- a/share/templates/roundcubemail/kolab_folders.inc.php.tpl
+++ b/share/templates/roundcubemail/kolab_folders.inc.php.tpl
@@ -2,9 +2,11 @@
     \$config['kolab_folders_configuration_default'] = 'Configuration';
     \$config['kolab_folders_event_default'] = 'Calendar';
     \$config['kolab_folders_contact_default'] = 'Contacts';
-    \$config['kolab_folders_task_default'] = '';
-    \$config['kolab_folders_note_default'] = '';
-    \$config['kolab_folders_journal_default'] = '';
+    \$config['kolab_folders_task_default'] = 'Tasks';
+    \$config['kolab_folders_note_default'] = 'Notes';
+    \$config['kolab_folders_file_default'] = 'Files';
+    \$config['kolab_folders_freebusy_default'] = 'Freebusy';
+    \$config['kolab_folders_journal_default'] = 'Journal';
     \$config['kolab_folders_mail_inbox'] = 'INBOX';
     \$config['kolab_folders_mail_drafts'] = 'Drafts';
     \$config['kolab_folders_mail_sentitems'] = 'Sent';


commit ec2dc345c803ed50dfd81c497ba85b8ed45b31c9
Author: Aeneas Jaißle <aj at ajaissle.de>
Date:   Tue Aug 19 11:36:11 2014 +0200

    Add /etc/amavisd.conf as possible amavisd configuration file

diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
index f38ee07..166b402 100644
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@ -388,6 +388,8 @@ result_format = shared+%%s
             fp = open('/etc/amavisd/amavisd.conf', 'w')
         elif os.path.isdir('/etc/amavis'):
             fp = open('/etc/amavis/amavisd.conf', 'w')
+        elif os.path.isfile('/etc/amavisd.conf'):
+            fp = open('/etc/amavisd.conf', 'w')
 
         if not fp == None:
             fp.write(t.__str__())


commit f40f94e60cdb3b07c76408acf9d64f680a0b82ab
Author: Aeneas Jaißle <aj at ajaissle.de>
Date:   Tue Aug 19 11:35:02 2014 +0200

    This patch adds an option '--socket' that can be used when starting kolab-saslauthd, to specify the socket file to bind to.

diff --git a/pykolab/conf/__init__.py b/pykolab/conf/__init__.py
index 030a626..e05f140 100644
--- a/pykolab/conf/__init__.py
+++ b/pykolab/conf/__init__.py
@@ -590,6 +590,16 @@ class Conf(object):
                     except IOError, e:
                         log.error(_("Cannot start SASL authentication daemon"))
                         return False
+            elif os.path.isfile("/var/run/sasl2/mux"):
+                if os.path.isfile("/var/run/sasl2/saslauthd.pid"):
+                    log.error(_("Cannot start SASL authentication daemon"))
+                    return False
+                else:
+                    try:
+                        os.remove("/var/run/sasl2/mux")
+                    except IOError, e:
+                        log.error(_("Cannot start SASL authentication daemon"))
+                        return False
         return True
 
     def check_setting_use_imap(self, value):
diff --git a/saslauthd/__init__.py b/saslauthd/__init__.py
index 32927a8..b7f81d5 100644
--- a/saslauthd/__init__.py
+++ b/saslauthd/__init__.py
@@ -68,6 +68,15 @@ class SASLAuthDaemon(object):
             )
 
         daemon_group.add_option(
+                "-s",
+                "--socket",
+                dest    = "socketfile",
+                action  = "store",
+                default = "/var/run/saslauthd/mux",
+                help    = _("Socket file to bind to.")
+            )
+
+        daemon_group.add_option(
                 "-u",
                 "--user",
                 dest    = "process_username",
diff --git a/saslauthd/kolab-saslauthd.sysvinit b/saslauthd/kolab-saslauthd.sysvinit
index 033bbc7..5090a65 100644
--- a/saslauthd/kolab-saslauthd.sysvinit
+++ b/saslauthd/kolab-saslauthd.sysvinit
@@ -24,7 +24,11 @@ if [ -f  /etc/init.d/functions ]; then
 fi
 
 # Source our configuration file for these variables.
+if [[ -d /var/run/sasl2 ]]; then
+SOCKETDIR=/var/run/sasl2
+else
 SOCKETDIR=/var/run/saslauthd
+fi
 FLAGS="--fork -l warning"
 
 if [ -f /etc/sysconfig/kolab-saslauthd ] ; then


commit b7448315c76685b66a74368aa6bbfd70c5d7b531
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 10:50:57 2014 +0200

    Allow a URI to be passed on to connect()

diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index 9549fe8..2199e9f 100644
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@ -74,8 +74,24 @@ def authenticate(username=None, password=None, domain=None):
         session_id = response['session_token']
         return True
 
-def connect():
-    global conn
+def connect(uri=None):
+    global conn, API_SSL, API_PORT, API_HOSTNAME, API_BASE
+
+    if not uri == None:
+        result = urlparse(uri)
+
+        if hasattr(result, 'scheme') and result.scheme == 'https':
+            API_SSL = True
+            API_PORT = 443
+
+        if hasattr(result, 'hostname'):
+            API_HOSTNAME = result.hostname
+
+        if hasattr(result, 'port'):
+            API_PORT = result.port
+
+        if hasattr(result, 'path'):
+            API_BASE = result.path
 
     if conn == None:
         if API_SSL:


commit 2c3a0384bd5fca829a1e7b2b0de3dcb209169052
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Aug 19 10:50:25 2014 +0200

    clamd.amavisd is actually clamd at amavisd

diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
index 7cd1919..f38ee07 100644
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@ -421,7 +421,7 @@ result_format = shared+%%s
     if os.path.isfile('/bin/systemctl'):
         subprocess.call(['systemctl', 'restart', 'postfix.service'])
         subprocess.call(['systemctl', 'restart', 'amavisd.service'])
-        subprocess.call(['systemctl', 'restart', 'clamd.amavisd.service'])
+        subprocess.call(['systemctl', 'restart', 'clamd at amavisd.service'])
         subprocess.call(['systemctl', 'restart', 'wallace.service'])
     elif os.path.isfile('/sbin/service'):
         subprocess.call(['service', 'postfix', 'restart'])
@@ -439,7 +439,7 @@ result_format = shared+%%s
     if os.path.isfile('/bin/systemctl'):
         subprocess.call(['systemctl', 'enable', 'postfix.service'])
         subprocess.call(['systemctl', 'enable', 'amavisd.service'])
-        subprocess.call(['systemctl', 'enable', 'clamd.amavisd.service'])
+        subprocess.call(['systemctl', 'enable', 'clamd at amavisd.service'])
         subprocess.call(['systemctl', 'enable', 'wallace.service'])
     elif os.path.isfile('/sbin/chkconfig'):
         subprocess.call(['chkconfig', 'postfix', 'on'])


commit c5e8514c602be65eec3bec91ab16ffddf15c9148
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 15:35:46 2014 +0200

    Improve the defaults for iTip sending on behalf of users not logged in.

diff --git a/share/templates/roundcubemail/calendar.inc.php.tpl b/share/templates/roundcubemail/calendar.inc.php.tpl
index 357c8ce..6ee1506 100644
--- a/share/templates/roundcubemail/calendar.inc.php.tpl
+++ b/share/templates/roundcubemail/calendar.inc.php.tpl
@@ -9,6 +9,10 @@
     \$config['calendar_event_coloring'] = 0;
     \$config['calendar_caldav_url'] = 'http://' . \$_SERVER['HTTP_HOST'] . '/iRony/calendars/%u/%i';
 
+    \$config['calendar_itip_smtp_server'] = '';
+    \$config['calendar_itip_smtp_user'] = '';
+    \$config['calendar_itip_smtp_pass'] = '';
+
     \$config['calendar_contact_birthdays'] = true;
 
     \$config['calendar_resources_driver'] = 'ldap';


commit a526ac76f0269c4c8216d308f9d616d74a123a27
Author: Timotheus Pokorra (TBits.net) <tp at tbits.net>
Date:   Thu Aug 14 14:51:09 2014 +0200

    killproc -p $pidfile to only kill the master thread (#917)

diff --git a/wallace/wallace.sysvinit b/wallace/wallace.sysvinit
index 4848827..64109f8 100644
--- a/wallace/wallace.sysvinit
+++ b/wallace/wallace.sysvinit
@@ -58,7 +58,7 @@ start() {
 
 stop() {
     echo -n $"Stopping $prog: "
-    killproc $prog
+    killproc -p $pidfile $prog
     RETVAL=$?
     echo
     [ $RETVAL -eq 0 ] && rm -f $lockfile


commit b006de19c34ef3678d30fa67c88baf5ce300f733
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 14:47:05 2014 +0200

    Correct the folder path at the earliest opportunity to avoid mis-firing set acl commands (#3315)

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index c276403..23b80e8 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1214,6 +1214,9 @@ class LDAP(pykolab.base.Base):
             else:
                 folder_path = entry['cn']
 
+        if not folder_path.startswith('shared/'):
+            folder_path = "shared/%s" % folder_path
+
         folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute')
         if folderacl_entry_attribute == None:
             folderacl_entry_attribute = 'acl'
@@ -1593,6 +1596,9 @@ class LDAP(pykolab.base.Base):
             else:
                 folder_path = entry['cn']
 
+        if not folder_path.startswith('shared/'):
+            folder_path = "shared/%s" % folder_path
+
         folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute')
         if folderacl_entry_attribute == None:
             folderacl_entry_attribute = 'acl'
@@ -1802,6 +1808,9 @@ class LDAP(pykolab.base.Base):
             else:
                 folder_path = entry['cn']
 
+        if not folder_path.startswith('shared/'):
+            folder_path = "shared/%s" % folder_path
+
         if not self.imap.shared_folder_exists(folder_path):
             self.imap.shared_folder_create(folder_path, server)
 


commit 7edf06594b70b6fc203e01341b573a07e65875cf
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 14:30:35 2014 +0200

    Set version to 0.7.0

diff --git a/configure.ac b/configure.ac
index 7ae2519..a783e77 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,5 +1,5 @@
-AC_INIT([pykolab], 0.7)
-AC_SUBST([RELEASE], 0.1)
+AC_INIT([pykolab], 0.7.0)
+AC_SUBST([RELEASE], 1)
 
 AC_CONFIG_SRCDIR(pykolab/constants.py.in)
 


commit b5f2d9e385cda35c9cb8b068c25d4d330de52ec7
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 14:02:13 2014 +0200

    Only break when an actual schema file has been found

diff --git a/pykolab/setup/setup_roundcube.py b/pykolab/setup/setup_roundcube.py
index 56110cb..259f676 100644
--- a/pykolab/setup/setup_roundcube.py
+++ b/pykolab/setup/setup_roundcube.py
@@ -149,7 +149,8 @@ def execute(*args, **kw):
                             if not schema_filepath in schema_files:
                                 schema_files.append(schema_filepath)
 
-                break
+                if len(schema_files) > 0:
+                    break
         break
 
     if os.path.isdir('/usr/share/roundcubemail'):


commit aa266861ccbffb4ab5f9176d4d46b569771c992a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 13:44:38 2014 +0200

    Enable wallace by default

diff --git a/share/templates/master.cf.tpl b/share/templates/master.cf.tpl
index 18aeea5..114dca1 100644
--- a/share/templates/master.cf.tpl
+++ b/share/templates/master.cf.tpl
@@ -72,7 +72,7 @@ smtp-amavis         unix        -       -       n       -       3       smtp
 # Listener to re-inject email from Amavisd into Postfix
 127.0.0.1:10025     inet        n       -       n       -       100     smtpd
     -o cleanup_service_name=cleanup_internal
-    -o content_filter=
+    -o content_filter=smtp-wallace:[127.0.0.1]:10026
     -o local_recipient_maps=
     -o relay_recipient_maps=
     -o smtpd_restriction_classes=


commit b35567b8e2c24c69f56eb59b986e3f4f6c04be12
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 13:38:57 2014 +0200

    kolab-users -> kolab-people

diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py
index ff15293..dfabab5 100644
--- a/pykolab/setup/setup_freebusy.py
+++ b/pykolab/setup/setup_freebusy.py
@@ -91,7 +91,7 @@ def execute(*args, **kw):
                     'fbsource': 'file:/var/cache/kolab-freebusy/%s.ifb',
                     'expires': '15m'
                 },
-            'directory "kolab-users"': {
+            'directory "kolab-people"': {
                     'type': 'ldap',
                     'host': conf.get('ldap', 'ldap_uri'),
                     'base_dn': conf.get('ldap', 'base_dn'),


commit a7c5e671b0531ac2ea390ebc0f1f604218d62718
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 13:23:26 2014 +0200

    Use ldap_simple as the default password driver

diff --git a/share/templates/roundcubemail/password.inc.php.tpl b/share/templates/roundcubemail/password.inc.php.tpl
index ca3f815..16895fa 100644
--- a/share/templates/roundcubemail/password.inc.php.tpl
+++ b/share/templates/roundcubemail/password.inc.php.tpl
@@ -4,7 +4,7 @@
     // -----------------------
     // A driver to use for password change. Default: "sql".
     // See README file for list of supported driver names.
-    \$config['password_driver'] = 'ldap';
+    \$config['password_driver'] = 'ldap_simple';
 
     // Determine whether current password is required to change password.
     // Default: false.


commit 6e91426d541921efd39c7cc12a8372848c312ed4
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Aug 14 11:17:30 2014 +0200

    Reflect the appropriate defaults for the Kolab Free/Busy web service

diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py
index 9496651..ff15293 100644
--- a/pykolab/setup/setup_freebusy.py
+++ b/pykolab/setup/setup_freebusy.py
@@ -79,16 +79,25 @@ def execute(*args, **kw):
         scheme = 'imaps'
 
     resources_imap_uri = '%s://%s:%s@%s:%s/%%kolabtargetfolder?acl=lrs' % (scheme, admin_login, admin_password, hostname, port)
-    users_imap_uri = '%s://%%mail:%s@%s:%s/?proxy_auth=%s' % (scheme, admin_password, hostname, port, admin_login)
+    users_imap_uri = '%s://%%s:%s@%s:%s/?proxy_auth=%s' % (scheme, admin_password, hostname, port, admin_login)
 
     freebusy_settings = {
+            'directory "local"': {
+                    'type': 'static',
+                    'fbsource': 'file:/var/lib/kolab-freebusy/%s.ifb',
+                },
+            'directory "local-cache"': {
+                    'type': 'static',
+                    'fbsource': 'file:/var/cache/kolab-freebusy/%s.ifb',
+                    'expires': '15m'
+                },
             'directory "kolab-users"': {
                     'type': 'ldap',
                     'host': conf.get('ldap', 'ldap_uri'),
                     'base_dn': conf.get('ldap', 'base_dn'),
                     'bind_dn': conf.get('ldap', 'service_bind_dn'),
                     'bind_pw': conf.get('ldap', 'service_bind_pw'),
-                    'filter': '(&(objectClass=kolabInetOrgPerson)(|(uid=%s)(mail=%s)(alias=%s)))',
+                    'filter': '(&(objectClass=kolabInetOrgPerson)(|(mail=%s)(alias=%s)))',
                     'attributes': 'mail',
                     'lc_attributes': 'mail',
                     'fbsource': users_imap_uri,
@@ -103,11 +112,11 @@ def execute(*args, **kw):
                     'bind_dn': conf.get('ldap', 'service_bind_dn'),
                     'bind_pw': conf.get('ldap', 'service_bind_pw'),
                     'attributes': 'mail, kolabtargetfolder',
-                    'filter': '(&(objectClass=kolabsharedfolder)(mail=%s))',
+                    'filter': '(&(objectClass=kolabsharedfolder)(kolabfoldertype=event)(mail=%s))',
                     'fbsource': resources_imap_uri,
                     'cacheto': '/var/cache/kolab-freebusy/%mail.ifb',
                     'expires': '15m',
-                    'loglevel': 300
+                    'loglevel': 300,
                 },
         }
 


commit 262f1b2b2fb3831707dbb3f5948736a6c5ca5d3f
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 14 03:14:38 2014 -0400

    Fix traceback errors in resource booking module (#3312)

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index d1f792b..55d7472 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -321,7 +321,8 @@ def execute(*args, **kw):
         # process CANCEL messages
         if not done and itip_event['method'] == "CANCEL":
             for resource in resource_dns:
-                if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
+                if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()] \
+                    and resources[resource].has_key('kolabtargetfolder'):
                     delete_resource_event(itip_event['uid'], resources[resource])
 
             done = True
@@ -446,6 +447,8 @@ def check_availability(itip_events, resource_dns, resources, receiving_attendee=
         if len(resources[resource]['conflicting_events']) > 0:
             log.debug(_("Conflicting events: %r for resource %r") % (resources[resource]['conflicting_events'], resource), level=9)
 
+            done = False
+
             # This is the event being conflicted with!
             for itip_event in itip_events:
                 # Now we have the event that was conflicting


commit 3cc6d87fb7ac57322e4e8b7d0321f280a49344cc
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 13 16:29:50 2014 +0200

    Supply the default configuration for sharedfolder_acl_entry_attribute

diff --git a/conf/kolab.conf b/conf/kolab.conf
index c97757b..80d1f53 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -210,6 +210,9 @@ kolab_group_filter = (|(objectclass=kolabgroupofuniquenames)(objectclass=kolabgr
 sharedfolder_base_dn = ou=Shared Folders,%(base_dn)s
 sharedfolder_filter = (objectclass=kolabsharedfolder)
 
+; The attribute entry name that controls the ACLs set on a shared folder
+sharedfolder_acl_entry_attribute = acl
+
 ; Same again. Resources live in a different OU structure or;
 ;
 ; - They would appear in the address book(s) as distribution lists or individual contacts,


commit b52a188db9dbc7ed4d8d04a92d66c86e61f9dcfa
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 13 16:28:46 2014 +0200

    Use the correct (actually configured) acl entry attribute for shared folders

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index f15d2c8..c276403 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1773,7 +1773,7 @@ class LDAP(pykolab.base.Base):
                     'kolabfoldertype'
                 )
 
-        folderacl_entry_attribute = conf.get('ldap', 'folderacl_entry_attribute')
+        folderacl_entry_attribute = conf.get('ldap', 'sharedfolder_acl_entry_attribute')
         if folderacl_entry_attribute == None:
             folderacl_entry_attribute = 'acl'
 


commit eeb52be1e300509cb1129e6951e7451cd441957c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 13 16:28:26 2014 +0200

    Do not fail as miserably if setting an ACL fails

diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index f52dc9f..b3af455 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -364,7 +364,16 @@ class IMAP(object):
             _acl = [x for x in _acl.split() if x not in acl_map['subtract'].split()]
             acl = ''.join(list(set(_acl)))
 
-        self.imap.sam(self.folder_utf7(folder), identifier, acl)
+        try:
+            self.imap.sam(self.folder_utf7(folder), identifier, acl)
+        except Exception, errmsg:
+            log.error(
+                    _("Could not set ACL for %s on folder %s: %r") % (
+                            identifier,
+                            folder,
+                            errmsg
+                        )
+                )
 
     def set_metadata(self, folder, metadata_path, metadata_value, shared=True):
         """


commit 3bf16dfc05fcad3cb01bf0a3b21611db4839e8ce
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Wed Aug 13 16:27:51 2014 +0200

    Supply the default value for imap/virtual_domains

diff --git a/conf/kolab.conf b/conf/kolab.conf
index 128f0b8..c97757b 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -136,6 +136,9 @@ autocreate_folders = {
             },
     }
 
+[imap]
+virtual_domains = userid
+
 [ldap]
 ; The URI to LDAP
 ldap_uri = ldap://localhost:389


commit 89e6d4b1c8cc0d428ea7d35a394de324d9077ea9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 7 11:29:01 2014 -0400

    Updated translation source files

diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5a5bc37..42d9803 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -42,7 +42,9 @@ pykolab/cli/cmd_list_mailbox_acls.py
 pykolab/cli/cmd_list_mailboxes.py
 pykolab/cli/cmd_list_mailbox_metadata.py
 pykolab/cli/cmd_list_messages.py
+pykolab/cli/cmd_list_ous.py
 pykolab/cli/cmd_list_quota.py
+pykolab/cli/cmd_list_users.py
 pykolab/cli/cmd_list_user_subscriptions.py
 pykolab/cli/cmd_mailbox_cleanup.py
 pykolab/cli/cmd_remove_mailaddress.py
@@ -112,6 +114,7 @@ pykolab/xml/contact.py
 pykolab/xml/contact_reference.py
 pykolab/xml/event.py
 pykolab/xml/__init__.py
+pykolab/xml/recurrence_rule.py
 pykolab/xml/utils.py
 saslauthd/__init__.py
 saslauthd.py
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 1966fae..feb5583 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1,10 +1,13 @@
+bin/._kolab_smtp_access_policy.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_delete_message.py
 pykolab/cli/._cmd_list_mailbox_metadata.py
+pykolab/cli/._cmd_list_messages.py
 pykolab/cli/._cmd_list_quota.py
 pykolab/cli/._cmd_set_language.py
 pykolab/cli/._cmd_set_mailbox_acl.py
@@ -32,8 +35,11 @@ pykolab/._translit.py
 pykolab/._utils.py
 pykolab/wap_client/.___init__.py
 pykolab/xml/._attendee.py
+pykolab/xml/._contact.py
+pykolab/xml/._contact_reference.py
 pykolab/xml/._event.py
 pykolab/xml/.___init__.py
+pykolab/xml/._recurrence_rule.py
 pykolab/xml/._utils.py
 tests/functional/._purge_users.py
 tests/functional/._resource_func.py
diff --git a/po/pykolab.pot b/po/pykolab.pot
index 389ca9b..6e4a662 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-07-10 07:21-0400\n"
+"POT-Creation-Date: 2014-08-07 11:26-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"
@@ -368,7 +368,7 @@ msgstr ""
 msgid "Could not connect to LDAP, is it running?"
 msgstr ""
 
-#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137
+#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2166
 #: ../pykolab/cli/cmd_sync.py:36
 msgid "Listing domains..."
 msgstr ""
@@ -668,99 +668,99 @@ msgstr ""
 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
+#: ../pykolab/auth/ldap/__init__.py:1237 ../pykolab/auth/ldap/__init__.py:1254
+#: ../pykolab/auth/ldap/__init__.py:1616 ../pykolab/auth/ldap/__init__.py:1633
 #, python-format
 msgid "Found a subject %r with access %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1356
+#: ../pykolab/auth/ldap/__init__.py:1357
 #, python-format
 msgid "Entry %s attribute value: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1364
+#: ../pykolab/auth/ldap/__init__.py:1365
 #, python-format
 msgid "imap.user_mailbox_server(%r) result: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853
+#: ../pykolab/auth/ldap/__init__.py:1685 ../pykolab/auth/ldap/__init__.py:1882
 #, python-format
 msgid "Result from recipient policy: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:1908
+#: ../pykolab/auth/ldap/__init__.py:1937
 #, python-format
 msgid "Kolab user %s does not have a result attribute %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2067
+#: ../pykolab/auth/ldap/__init__.py:2096
 #, python-format
 msgid "Finding domain root dn for domain %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2164
+#: ../pykolab/auth/ldap/__init__.py:2193
 msgid "Authentication database DOWN"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296
+#: ../pykolab/auth/ldap/__init__.py:2277 ../pykolab/auth/ldap/__init__.py:2325
 #, python-format
 msgid "Entry type: %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2321
-#, python-format
-msgid "Done with _synchronize_callback() for entry %r"
-msgstr ""
-
-#: ../pykolab/auth/ldap/__init__.py:2393
+#: ../pykolab/auth/ldap/__init__.py:2414
 msgid "LDAP Search Result Data Entry:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2409
+#: ../pykolab/auth/ldap/__init__.py:2430
 msgid "Entry Change Notification attributes:"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2414
+#: ../pykolab/auth/ldap/__init__.py:2435
 #, python-format
 msgid "Change Type: %r (%r)"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2422
+#: ../pykolab/auth/ldap/__init__.py:2443
 #, python-format
 msgid "Previous DN: %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2477
+#: ../pykolab/auth/ldap/__init__.py:2498
 #, python-format
 msgid "Object %s searched no longer exists"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2487
+#: ../pykolab/auth/ldap/__init__.py:2508
 #, python-format
 msgid "%d results..."
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2590
+#: ../pykolab/auth/ldap/__init__.py:2611
 #, python-format
 msgid "Searching with filter %r"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2642
+#: ../pykolab/auth/ldap/__init__.py:2663
 #, python-format
 msgid "Checking for support for %s on %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2661
+#: ../pykolab/auth/ldap/__init__.py:2682
 #, python-format
 msgid "Found support for %s"
 msgstr ""
 
-#: ../pykolab/auth/ldap/__init__.py:2706
+#: ../pykolab/auth/ldap/__init__.py:2727
 #, python-format
 msgid "An error occured using %s: %r"
 msgstr ""
 
+#: ../pykolab/auth/ldap/__init__.py:2733
+#, python-format
+msgid "%s"
+msgstr ""
+
 #: ../pykolab/auth/ldap/syncrepl.py:46
 msgid "The name of the persistent, unique attribute "
 msgstr ""
@@ -941,6 +941,11 @@ msgstr ""
 msgid "No such folder(s)"
 msgstr ""
 
+#: ../pykolab/cli/cmd_delete_mailbox.py:63
+#, python-format
+msgid "Could not delete mailbox '%s'"
+msgstr ""
+
 #: ../pykolab/cli/cmd_delete_message.py:36
 msgid "Delete a message from a folder"
 msgstr ""
@@ -1190,27 +1195,27 @@ msgstr ""
 
 #. This is a nested command
 #. This is a nested component
-#: ../pykolab/cli/commands.py:98 ../pykolab/setup/components.py:90
+#: ../pykolab/cli/commands.py:97 ../pykolab/setup/components.py:90
 #, python-format
 msgid "Command Group: %s"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:113 ../pykolab/cli/commands.py:118
+#: ../pykolab/cli/commands.py:112 ../pykolab/cli/commands.py:117
 msgid "No such command."
 msgstr ""
 
-#: ../pykolab/cli/commands.py:168 ../pykolab/setup/components.py:231
+#: ../pykolab/cli/commands.py:167 ../pykolab/setup/components.py:231
 #, python-format
 msgid "Command '%s' already registered"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:193 ../pykolab/setup/components.py:257
+#: ../pykolab/cli/commands.py:192 ../pykolab/setup/components.py:257
 #: ../wallace/modules.py:369
 #, python-format
 msgid "Alias for %s"
 msgstr ""
 
-#: ../pykolab/cli/commands.py:201 ../pykolab/setup/components.py:265
+#: ../pykolab/cli/commands.py:200 ../pykolab/setup/components.py:265
 msgid "Not yet implemented"
 msgstr ""
 
@@ -1538,84 +1543,89 @@ msgstr ""
 msgid "Could not connect to Cyrus IMAP server %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:137
+#: ../pykolab/imap/cyrus.py:138
 #, python-format
 msgid "Continuing with separator: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:142
+#: ../pykolab/imap/cyrus.py:143
 msgid "Detected we are running in a Murder topology"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:146
+#: ../pykolab/imap/cyrus.py:147
 msgid "This system is not part of a murder topology"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:167
+#: ../pykolab/imap/cyrus.py:168
 #, python-format
 msgid "Checking actual backend server for folder %s through annotations"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:172
+#: ../pykolab/imap/cyrus.py:173
 msgid "Possibly reproducing the find "
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:195
+#: ../pykolab/imap/cyrus.py:196
 #, python-format
 msgid "Could not get the annotations after %s tries."
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:199
+#: ../pykolab/imap/cyrus.py:200
 #, python-format
 msgid "No annotations for %s: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:206
+#: ../pykolab/imap/cyrus.py:207
 #, python-format
 msgid "Server for INBOX folder %s is %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:226
+#: ../pykolab/imap/cyrus.py:227
 #, python-format
 msgid "Setting quota for folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:230
+#: ../pykolab/imap/cyrus.py:231
 #, python-format
 msgid "Could not set quota for mailfolder %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:239
+#: ../pykolab/imap/cyrus.py:241
+#, python-format
+msgid "Moving INBOX folder %s to %s on partition %s"
+msgstr ""
+
+#: ../pykolab/imap/cyrus.py:243
 #, python-format
 msgid "Moving INBOX folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:254
+#: ../pykolab/imap/cyrus.py:259
 #, python-format
 msgid "Setting annotation %s on folder %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:259
+#: ../pykolab/imap/cyrus.py:264
 #, python-format
 msgid "Could not set annotation %r on mail folder %r: %r"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:263
+#: ../pykolab/imap/cyrus.py:268
 #, python-format
 msgid "Transferring folder %s from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:323
+#: ../pykolab/imap/cyrus.py:328
 #, python-format
 msgid "Undeleting %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:334
+#: ../pykolab/imap/cyrus.py:339
 #, python-format
 msgid "Would have transfered %s from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/cyrus.py:336
+#: ../pykolab/imap/cyrus.py:341
 #, python-format
 msgid "Would have renamed %s to %s"
 msgstr ""
@@ -1674,189 +1684,189 @@ msgstr ""
 msgid "Called imap.disconnect() on a server that we had no connection to."
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:222 ../pykolab/imap/__init__.py:234
+#: ../pykolab/imap/__init__.py:221 ../pykolab/imap/__init__.py:233
 #, python-format
 msgid "Could not create folder %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:223
+#: ../pykolab/imap/__init__.py:222
 #, python-format
 msgid " on server %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:244 ../pykolab/imap/__init__.py:246
+#: ../pykolab/imap/__init__.py:243 ../pykolab/imap/__init__.py:245
 #, python-format
 msgid "%r has no attribute %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428
+#: ../pykolab/imap/__init__.py:396 ../pykolab/imap/__init__.py:431
 #, python-format
 msgid "Creating new shared folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675
+#: ../pykolab/imap/__init__.py:456 ../pykolab/imap/__init__.py:678
 #, python-format
 msgid "Downcasing mailbox name %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:457
+#: ../pykolab/imap/__init__.py:460
 #, python-format
 msgid "Creating new mailbox for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:470
+#: ../pykolab/imap/__init__.py:473
 msgid "Waiting for the Cyrus IMAP Murder to settle..."
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:516
+#: ../pykolab/imap/__init__.py:519
 #, python-format
 msgid "Creating additional folders for user %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:535
+#: ../pykolab/imap/__init__.py:538
 #, python-format
 msgid "Waiting for the Cyrus murder to settle... %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:547
+#: ../pykolab/imap/__init__.py:550
 #, python-format
 msgid "Correcting additional folder name from %r to %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:553
+#: ../pykolab/imap/__init__.py:556
 #, python-format
 msgid "Mailbox already exists: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:593
+#: ../pykolab/imap/__init__.py:596
 msgid "Subscribing user to the additional folders"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:607
+#: ../pykolab/imap/__init__.py:610
 msgid "Using the following tests for folder subscriptions:"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:609
+#: ../pykolab/imap/__init__.py:612
 #, python-format
 msgid "    %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:612
+#: ../pykolab/imap/__init__.py:615
 #, python-format
 msgid "Folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:624
+#: ../pykolab/imap/__init__.py:627
 #, python-format
 msgid "Subscribing %s to folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:628
+#: ../pykolab/imap/__init__.py:631
 #, python-format
 msgid "Subscribing %s to folder %s failed: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:658
+#: ../pykolab/imap/__init__.py:661
 #, python-format
 msgid "Could not rename %s to reside on partition %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:691
+#: ../pykolab/imap/__init__.py:694
 #, python-format
 msgid "INBOX folder to rename (%s) does not exist"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770
+#: ../pykolab/imap/__init__.py:697 ../pykolab/imap/__init__.py:773
 #, python-format
 msgid "Renaming INBOX from %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:698
+#: ../pykolab/imap/__init__.py:701
 #, python-format
 msgid "Could not rename INBOX folder %s to %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774
+#: ../pykolab/imap/__init__.py:703 ../pykolab/imap/__init__.py:777
 #, python-format
 msgid "Moving INBOX folder %s won't succeed as target folder %s already exists"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:704
+#: ../pykolab/imap/__init__.py:707
 #, python-format
 msgid "Server for mailbox %r is %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:712
+#: ../pykolab/imap/__init__.py:715
 #, python-format
 msgid "Looking for folder '%s', we found folders: %r"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:735
+#: ../pykolab/imap/__init__.py:738
 #, python-format
 msgid "Setting ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:746
+#: ../pykolab/imap/__init__.py:749
 #, python-format
 msgid "Removing ACL rights %s for subject %s on folder "
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:767
+#: ../pykolab/imap/__init__.py:770
 #, python-format
 msgid "Found old INBOX folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:776
+#: ../pykolab/imap/__init__.py:779
 #, python-format
 msgid "Did not find old folder user/%s to rename"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:778
+#: ../pykolab/imap/__init__.py:781
 msgid "Value for user is not a dictionary"
 msgstr ""
 
 #. TODO: Go in fact correct the quota.
-#: ../pykolab/imap/__init__.py:846
+#: ../pykolab/imap/__init__.py:849
 #, python-format
 msgid "Cannot get current IMAP quota for folder %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:859
+#: ../pykolab/imap/__init__.py:862
 #, python-format
 msgid "Quota for %s currently is %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:865
+#: ../pykolab/imap/__init__.py:868
 #, python-format
 msgid "Adjusting authentication database quota for folder %s to %d"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:870
+#: ../pykolab/imap/__init__.py:873
 #, python-format
 msgid "Correcting quota for %s to %s (currently %s)"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:947
+#: ../pykolab/imap/__init__.py:950
 #, python-format
 msgid "Checking folder: %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:952
+#: ../pykolab/imap/__init__.py:955
 #, python-format
 msgid "Folder has no corresponding user (1): %s"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:955
+#: ../pykolab/imap/__init__.py:958
 #, python-format
 msgid "Folder has no corresponding user (2): %s"
 msgstr ""
 
 #. We got user identifier only
-#: ../pykolab/imap/__init__.py:970
+#: ../pykolab/imap/__init__.py:973
 msgid "Please don't give us just a user identifier"
 msgstr ""
 
-#: ../pykolab/imap/__init__.py:973
+#: ../pykolab/imap/__init__.py:976
 #, python-format
 msgid "Deleting folder %s"
 msgstr ""
@@ -1892,17 +1902,23 @@ msgstr ""
 msgid "Message is not an iTip message (non-multipart message)"
 msgstr ""
 
-#: ../pykolab/itip/__init__.py:225
+#: ../pykolab/itip/__init__.py:229
 #, 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
+#: ../pykolab/itip/__init__.py:240 ../pykolab/itip/__init__.py:284
+#: ../wallace/module_invitationpolicy.py:966
+#: ../wallace/module_resources.py:1131
 #, python-format
 msgid "SMTP sendmail error: %r"
 msgstr ""
 
+#: ../pykolab/itip/__init__.py:272
+#, python-format
+msgid "Failed to compose iTip request message: %r"
+msgstr ""
+
 #: ../pykolab/logger.py:173 ../pykolab/logger.py:179
 #, python-format
 msgid "Could not change permissions on %s: %r"
@@ -2645,18 +2661,18 @@ msgstr ""
 msgid "Could not translate %s using locale %s"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:320
+#: ../pykolab/wap_client/__init__.py:380
 #, python-format
 msgid "Requesting %r with params %r"
 msgstr ""
 
-#: ../pykolab/wap_client/__init__.py:328
+#: ../pykolab/wap_client/__init__.py:388
 #, python-format
 msgid "Got response: %r"
 msgstr ""
 
 #. Some data is not JSON
-#: ../pykolab/wap_client/__init__.py:334
+#: ../pykolab/wap_client/__init__.py:394
 msgid "Response data is not JSON"
 msgstr ""
 
@@ -2689,91 +2705,91 @@ msgstr ""
 msgid "In Process"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130
+#: ../pykolab/xml/attendee.py:131 ../pykolab/xml/attendee.py:153
 msgid "Not a valid attendee"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:115
+#: ../pykolab/xml/attendee.py:138
 msgid "No valid delegator references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:135
+#: ../pykolab/xml/attendee.py:158
 msgid "No valid delegatee references found"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:180
+#: ../pykolab/xml/attendee.py:218
 #, python-format
 msgid "Invalid cutype %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:192
+#: ../pykolab/xml/attendee.py:230
 #, python-format
 msgid "Invalid participant status %r"
 msgstr ""
 
-#: ../pykolab/xml/attendee.py:200
+#: ../pykolab/xml/attendee.py:238
 #, python-format
 msgid "Invalid role %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708
-#: ../pykolab/xml/event.py:751
+#: ../pykolab/xml/event.py:146 ../pykolab/xml/event.py:780
+#: ../pykolab/xml/event.py:823
 msgid "Event start needs datetime.date or datetime.datetime instance"
 msgstr ""
 
-#: ../pykolab/xml/event.py:241
+#: ../pykolab/xml/event.py:291
 #, python-format
 msgid "No attendee with email or name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:249
+#: ../pykolab/xml/event.py:299
 #, python-format
 msgid "Invalid argument value attendee %r, must be basestring or Attendee"
 msgstr ""
 
-#: ../pykolab/xml/event.py:255
+#: ../pykolab/xml/event.py:311
 #, python-format
 msgid "No attendee with email %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:261
+#: ../pykolab/xml/event.py:317
 #, python-format
 msgid "No attendee with name %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:426
+#: ../pykolab/xml/event.py:488
 msgid "Invalid participant status"
 msgstr ""
 
-#: ../pykolab/xml/event.py:542
-#, python-format
-msgid "Invalid status %r"
-msgstr ""
-
-#: ../pykolab/xml/event.py:550
+#: ../pykolab/xml/event.py:610
 #, python-format
 msgid "Invalid classification %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:577
+#: ../pykolab/xml/event.py:641
 msgid "Event end needs datetime.date or datetime.datetime instance"
 msgstr ""
 
-#: ../pykolab/xml/event.py:761
+#: ../pykolab/xml/event.py:651
+#, python-format
+msgid "Invalid custom property name %r"
+msgstr ""
+
+#: ../pykolab/xml/event.py:833
 #, python-format
 msgid "Invalid status set: %r"
 msgstr ""
 
-#: ../pykolab/xml/event.py:923
+#: ../pykolab/xml/event.py:1070
 msgid "No sender specified"
 msgstr ""
 
-#: ../pykolab/xml/event.py:932
+#: ../pykolab/xml/event.py:1079
 #, python-format
 msgid "Invitation for %s was %s"
 msgstr ""
 
-#: ../pykolab/xml/event.py:937
+#: ../pykolab/xml/event.py:1084
 msgid "This is an automated response to one of your event requests."
 msgstr ""
 
@@ -2795,23 +2811,40 @@ msgstr ""
 msgid "Maximum tries exceeded, exiting"
 msgstr ""
 
-#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190
-#: ../wallace/module_resources.py:879
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:192
+#: ../wallace/module_resources.py:1041
 #, 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
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:615
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:631
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:662
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:700
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:756
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:769
+#: ../wallace/module_resources.py:1121
 #, 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
+#. check confirmation message sent to resource owner (jane)
+#. check first confirmation message sent to resource owner (jane)
+#. check second confirmation message sent to resource owner (jane)
+#. check confirmation message sent to resource owner (jane)
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:652
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:690
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:728
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:745
+#: ../tests/functional/test_wallace/test_005_resource_invitation.py:799
+#: ../wallace/module_resources.py:1217
+#, python-format
+msgid "Booking request for %s requires confirmation"
+msgstr ""
+
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:167
+#: ../wallace/module_invitationpolicy.py:377
 #, python-format
 msgid "\"%(summary)s\" has been %(status)s"
 msgstr ""
@@ -2819,19 +2852,25 @@ 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
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:667
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:673
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:686
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:704
+#: ../wallace/module_invitationpolicy.py:955
 #, 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
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:678
+#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:690
 msgid "PENDING"
 msgstr ""
 
+#: ../tests/unit/test-011-itip.py:408
+#, python-format
+msgid "Invitation for %(summary)s was %(status)s"
+msgstr ""
+
 #: ../wallace/__init__.py:57
 #, python-format
 msgid "Wallace modules: %r"
@@ -2874,22 +2913,22 @@ msgid "Could not write pid file %s"
 msgstr ""
 
 #: ../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
+#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:61
+#: ../wallace/module_resources.py:125
 #, python-format
 msgid "Issuing callback after processing to stage %s"
 msgstr ""
 
 #: ../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
+#: ../wallace/module_invitationpolicy.py:174 ../wallace/module_optout.py:62
+#: ../wallace/module_resources.py:131
 #, python-format
 msgid "Testing cb_action_%s()"
 msgstr ""
 
 #: ../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
+#: ../wallace/module_invitationpolicy.py:176 ../wallace/module_optout.py:64
+#: ../wallace/module_resources.py:134
 #, python-format
 msgid "Attempting to execute cb_action_%s()"
 msgstr ""
@@ -2954,234 +2993,253 @@ msgstr ""
 msgid "An error occurred: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:154
+#: ../wallace/module_invitationpolicy.py:158
 #, python-format
 msgid "Invitation policy called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:211
-#: ../wallace/module_resources.py:169
+#: ../wallace/module_invitationpolicy.py:215
+#: ../wallace/module_resources.py:176
 #, python-format
 msgid "Failed to parse iTip events from message: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:215
+#: ../wallace/module_invitationpolicy.py:219
 msgid ""
 "Message is not an iTip message or does not contain any (valid) iTip events."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:219
+#: ../wallace/module_invitationpolicy.py:223
 #, python-format
 msgid ""
 "iTip events attached to this message contain the following information: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:232
+#: ../wallace/module_invitationpolicy.py:236
 #, python-format
 msgid "No itips, no users, pass along %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:235
+#: ../wallace/module_invitationpolicy.py:239
 #, python-format
 msgid "iTips, but no users, pass along %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:255
+#: ../wallace/module_invitationpolicy.py:259
 #, python-format
 msgid "No user attendee matching envelope recipient %s, skip message"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:259
+#: ../wallace/module_invitationpolicy.py:263
 #, python-format
 msgid "Receiving user: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:284
+#: ../wallace/module_invitationpolicy.py:287
 #, python-format
-msgid "Apply invitation policy %r for domain %r"
+msgid "Apply invitation policy %r for sender %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:295
+#: ../wallace/module_invitationpolicy.py:298
 #, python-format
 msgid "Ignoring '%s' iTip method"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:299
+#: ../wallace/module_invitationpolicy.py:302
 #, python-format
 msgid "iTip message %r consumed by the invitationpolicy module"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:315
+#: ../wallace/module_invitationpolicy.py:318
 msgid "Pass invitation for manual processing"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:320
+#: ../wallace/module_invitationpolicy.py:323
 #, python-format
 msgid "Receiving Attendee: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:339
+#: ../wallace/module_invitationpolicy.py:342
 #, python-format
 msgid "Existing event: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:350
+#: ../wallace/module_invitationpolicy.py:353
 #, python-format
 msgid "Precondition for event %r fulfilled: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:386
+#: ../wallace/module_invitationpolicy.py:389
 #, python-format
 msgid "No RSVP for recipient %r requested"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:412
+#: ../wallace/module_invitationpolicy.py:415
 msgid "Pass reply for manual processing"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:419
+#: ../wallace/module_invitationpolicy.py:422
 #, python-format
 msgid "Sender Attendee: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:431
+#: ../wallace/module_invitationpolicy.py:434
 #, 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
+#: ../wallace/module_invitationpolicy.py:440
 #, python-format
 msgid "Auto-updating event %r on iTip REPLY"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:459
-#: ../wallace/module_invitationpolicy.py:488
+#: ../wallace/module_invitationpolicy.py:465
+#, python-format
+msgid "Add delegatee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:468
+#, python-format
+msgid "Update existing delegatee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:473
+#, python-format
+msgid "Update delegator: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:490
+#: ../wallace/module_invitationpolicy.py:519
 msgid ""
 "The event referred by this reply was not found in the user's calendars. "
 "Forwarding to Inbox."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:472
+#: ../wallace/module_invitationpolicy.py:503
 msgid "Pass cancellation for manual processing"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:517
+#: ../wallace/module_invitationpolicy.py:548
 #, python-format
 msgid "Checking if email address %r belongs to a local user"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:522
+#: ../wallace/module_invitationpolicy.py:553
 #, python-format
 msgid "User DN: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:524
+#: ../wallace/module_invitationpolicy.py:555
 #, python-format
 msgid "No user record(s) found for %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:577
+#: ../wallace/module_invitationpolicy.py:608
 #, python-format
 msgid "User record doesn't have the mailbox attribute %r set"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:590
+#: ../wallace/module_invitationpolicy.py:621
 #, python-format
 msgid "IMAP proxy authentication failed: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:612
+#: ../wallace/module_invitationpolicy.py:643
 #, python-format
 msgid "List calendar folders for user %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:628
+#: ../wallace/module_invitationpolicy.py:659
 #, python-format
 msgid "IMAP metadata for %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:658
+#: ../wallace/module_invitationpolicy.py:689
 #, 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
+#: ../wallace/module_invitationpolicy.py:701
 #, python-format
-msgid "Failed to parse event from message %s/%s: %r"
+msgid "Failed to parse event from message %s/%s: %s"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:696
+#: ../wallace/module_invitationpolicy.py:727
 #, python-format
 msgid "Listing events from folder %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:715
+#: ../wallace/module_invitationpolicy.py:740
+#: ../wallace/module_resources.py:553 ../wallace/module_resources.py:601
+#, python-format
+msgid "Failed to parse event from message %s/%s: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:746
 #, python-format
 msgid "Existing event %r conflicts with invitation %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:722
-#: ../wallace/module_resources.py:344
+#: ../wallace/module_invitationpolicy.py:753
+#: ../wallace/module_resources.py:411
 #, python-format
 msgid "start: %r, end: %r, total: %r, messages: %d"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:748
+#: ../wallace/module_invitationpolicy.py:779
 #, python-format
 msgid "%r is locked, waiting..."
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:811
+#: ../wallace/module_invitationpolicy.py:842
 #, python-format
 msgid "Failed to save event: no calendar folder found for user %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:814
+#: ../wallace/module_invitationpolicy.py:845
 #, python-format
 msgid "Save event %r to user calendar %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:827
+#: ../wallace/module_invitationpolicy.py:858
 #, python-format
 msgid "Failed to save event to user calendar at %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:843
+#: ../wallace/module_invitationpolicy.py:874
 #, python-format
 msgid "Delete event %r in %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:863
+#: ../wallace/module_invitationpolicy.py:894
 #, python-format
 msgid "Compose participation status summary for event %r to user %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:901
+#: ../wallace/module_invitationpolicy.py:931
 #, python-format
 msgid ""
 "Waiting for more automated replies (got %d of %d); skipping notification"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:998
+#: ../wallace/module_invitationpolicy.py:1028
 #, python-format
 msgid "Updated %s's copy of %r: %r"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1001
+#: ../wallace/module_invitationpolicy.py:1031
 #, python-format
 msgid "Attendee %s's copy of %r not found"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1004
+#: ../wallace/module_invitationpolicy.py:1034
 #, python-format
 msgid "Attendee %r not found in LDAP"
 msgstr ""
 
-#: ../wallace/module_invitationpolicy.py:1008
+#: ../wallace/module_invitationpolicy.py:1038
 #, python-format
 msgid ""
 "\n"
@@ -3213,184 +3271,225 @@ msgstr ""
 msgid "Could not send request to optout_url %s"
 msgstr ""
 
-#: ../wallace/module_resources.py:110
+#: ../wallace/module_resources.py:115
 #, python-format
 msgid "Resource Management called for %r, %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:174
+#: ../wallace/module_resources.py:181
 msgid "Message is not an iTip message or does not contain any "
 msgstr ""
 
-#: ../wallace/module_resources.py:182
+#: ../wallace/module_resources.py:189
 msgid "iTip events attached to this message contain the "
 msgstr ""
 
-#: ../wallace/module_resources.py:205
+#: ../wallace/module_resources.py:219
 msgid "Not an iTip message, but sent to resource nonetheless. Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:213
+#: ../wallace/module_resources.py:227
 #, python-format
 msgid "No itips, no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:216
+#: ../wallace/module_resources.py:230
 #, python-format
 msgid "iTips, but no resources, pass along %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:225
+#: ../wallace/module_resources.py:239
 #, python-format
 msgid "No resource attendees matching envelope recipient %s, Reject message"
 msgstr ""
 
-#: ../wallace/module_resources.py:234
+#: ../wallace/module_resources.py:249
 #, python-format
 msgid "Resources: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:244
+#: ../wallace/module_resources.py:267
+#, python-format
+msgid "Sender Attendee: %r => %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:274
+#, python-format
+msgid ""
+"The iTip reply sequence (%r) doesn't match the referred event version (%r). "
+"Ignoring."
+msgstr ""
+
+#: ../wallace/module_resources.py:299
+#, python-format
+msgid "Event referenced by this REPLY (%r) not found in resource calendar"
+msgstr ""
+
+#: ../wallace/module_resources.py:302
+msgid "No event reference found in this REPLY. Ignoring."
+msgstr ""
+
+#: ../wallace/module_resources.py:311
 #, python-format
 msgid "Receiving Resource: %r; %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:252
+#: ../wallace/module_resources.py:319
 #, python-format
 msgid "Recipient %r is non-participant, ignoring message"
 msgstr ""
 
-#: ../wallace/module_resources.py:279
+#: ../wallace/module_resources.py:346
 #, python-format
 msgid "Accept invitation for individual resource %r / %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:308
+#: ../wallace/module_resources.py:375
 #, python-format
 msgid "Delegate invitation for resource collection %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:340
+#: ../wallace/module_resources.py:407
 #, python-format
 msgid "Failed to read resource calendar for %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:350
+#: ../wallace/module_resources.py:417
 #, python-format
 msgid "Polling for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:353
+#: ../wallace/module_resources.py:420
 #, python-format
 msgid "Resource %r has been popped from the list"
 msgstr ""
 
-#: ../wallace/module_resources.py:357
+#: ../wallace/module_resources.py:424
 msgid "Resource is a collection"
 msgstr ""
 
-#: ../wallace/module_resources.py:368
+#: ../wallace/module_resources.py:435
 #, python-format
 msgid "Removed conflicting resources from %r: (%r) => %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:380
+#: ../wallace/module_resources.py:447
 #, python-format
 msgid "Conflicting events: %r for resource %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:397
+#: ../wallace/module_resources.py:464
 #, python-format
 msgid "Delegate to another resource collection member: %r to %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:459
+#: ../wallace/module_resources.py:526
 #, python-format
 msgid "Checking events in resource folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:475
+#: ../wallace/module_resources.py:542
 #, python-format
 msgid "Fetching message UID %r from folder %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:498
+#: ../wallace/module_resources.py:565
 #, python-format
 msgid "Event %r conflicts with event %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:525
+#: ../wallace/module_resources.py:586
+#, python-format
+msgid "Searching %r for event %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:592
+#, python-format
+msgid "Failed to access resource calendar:: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:621
+#, python-format
+msgid "Apply invitation policies %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:640
 #, python-format
 msgid "Adding event to %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:573
+#: ../wallace/module_resources.py:694
 #, python-format
 msgid "Failed to save event to resource calendar at %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:590
+#: ../wallace/module_resources.py:711
 #, python-format
 msgid "Delete resource calendar object %r in %r: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:633
+#: ../wallace/module_resources.py:754
 #, 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
+#: ../wallace/module_resources.py:762 ../wallace/module_resources.py:836
+#: ../wallace/module_resources.py:870
 #, python-format
 msgid "Resource record(s): %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711
-#: ../wallace/module_resources.py:746
+#: ../wallace/module_resources.py:764 ../wallace/module_resources.py:838
+#: ../wallace/module_resources.py:873
 #, 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
+#: ../wallace/module_resources.py:768 ../wallace/module_resources.py:842
+#: ../wallace/module_resources.py:877
 #, python-format
 msgid "Resource record: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:667
+#: ../wallace/module_resources.py:788
 #, python-format
 msgid "Raw itip_events: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:675
+#: ../wallace/module_resources.py:796
 #, python-format
 msgid "Raw set of attendees: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:683
+#: ../wallace/module_resources.py:804
 #, python-format
 msgid "Raw set of resources: %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:702
+#: ../wallace/module_resources.py:809
+#, python-format
+msgid "Raw set of organizers: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:829
 #, python-format
 msgid "Checking if attendee %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752
+#: ../wallace/module_resources.py:845 ../wallace/module_resources.py:879
 msgid "Resource reservation made but no resource records found"
 msgstr ""
 
-#: ../wallace/module_resources.py:737
+#: ../wallace/module_resources.py:864
 #, python-format
 msgid "Checking if resource %r is a resource (collection)"
 msgstr ""
 
-#: ../wallace/module_resources.py:755
+#: ../wallace/module_resources.py:882
 msgid "The following resources are being referred to in the "
 msgstr ""
 
-#: ../wallace/module_resources.py:894
+#: ../wallace/module_resources.py:1047
 #, python-format
 msgid ""
 "\n"
@@ -3401,7 +3500,7 @@ msgid ""
 "            "
 msgstr ""
 
-#: ../wallace/module_resources.py:905
+#: ../wallace/module_resources.py:1066
 #, python-format
 msgid ""
 "\n"
@@ -3411,7 +3510,7 @@ msgid ""
 "    "
 msgstr ""
 
-#: ../wallace/module_resources.py:912
+#: ../wallace/module_resources.py:1073
 #, python-format
 msgid ""
 "\n"
@@ -3420,16 +3519,16 @@ msgid ""
 "        "
 msgstr ""
 
-#: ../wallace/module_resources.py:941
+#: ../wallace/module_resources.py:1104
 #, python-format
 msgid "Sending booking notification for event %r to %r from %r"
 msgstr ""
 
-#: ../wallace/module_resources.py:954
+#: ../wallace/module_resources.py:1121
 msgid "failed"
 msgstr ""
 
-#: ../wallace/module_resources.py:973
+#: ../wallace/module_resources.py:1140
 #, python-format
 msgid ""
 "\n"
@@ -3441,7 +3540,7 @@ msgid ""
 "        "
 msgstr ""
 
-#: ../wallace/module_resources.py:979
+#: ../wallace/module_resources.py:1146
 #, python-format
 msgid ""
 "\n"
@@ -3455,6 +3554,29 @@ msgid ""
 "        "
 msgstr ""
 
+#: ../wallace/module_resources.py:1190
+#, python-format
+msgid "Clone invitation for owner confirmation: %r from %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:1196
+#, python-format
+msgid ""
+"\n"
+"        A reservation request for %(resource)s requires your approval!\n"
+"        Please either accept or decline this invitation without saving it to "
+"your calendar.\n"
+"\n"
+"        The reservation request was sent from %(orgname)s <%(orgemail)s>.\n"
+"\n"
+"        Subject: %(summary)s.\n"
+"        Date: %(date)s\n"
+"        Participants: %(attendees)s\n"
+"\n"
+"        *** This is an automated message, please don't reply by email. ***\n"
+"    "
+msgstr ""
+
 #. This is a nested module
 #: ../wallace/modules.py:97
 #, python-format


commit 5df811dc79af13c9619c7ed20a58ff2481da6850
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 7 11:28:44 2014 -0400

    Resource invitation policies require owner to be defined

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f38ae31..d1f792b 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -616,7 +616,7 @@ def accept_reservation_request(itip_event, resource, delegator=None, confirmed=F
     owner = get_resource_owner(resource)
     confirmation_required = False
 
-    if not confirmed:
+    if not confirmed and owner:
         invitationpolicy = get_resource_invitationpolicy(resource)
         log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=9)
 





More information about the commits mailing list