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