summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Boddie <paul@boddie.org.uk>2014-09-04 19:10:35 +0200
committerPaul Boddie <paul@boddie.org.uk>2014-09-04 19:10:35 +0200
commit6809a2ea710971b2ce9a98c125f31a01278939ad (patch)
treebd98f17725d2c72a352251f55d67fa4bbfde0aeb
parent73f44a1e823eb897b9e68de776ef7cb542b38682 (diff)
parent439b16872c89843d85955d09e125c2355b69649a (diff)
downloadpykolab-6809a2ea710971b2ce9a98c125f31a01278939ad.tar.gz
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
-rw-r--r--INSTALL2
-rw-r--r--conf/kolab.conf10
-rw-r--r--configure.ac4
-rw-r--r--kolabd/__init__.py6
-rw-r--r--po/POTFILES.in5
-rw-r--r--po/POTFILES.skip8
-rw-r--r--po/pykolab.pot952
-rw-r--r--pykolab/auth/ldap/__init__.py23
-rw-r--r--pykolab/conf/__init__.py10
-rw-r--r--pykolab/conf/defaults.py3
-rw-r--r--pykolab/imap/__init__.py13
-rw-r--r--pykolab/itip/__init__.py28
-rw-r--r--pykolab/logger.py34
-rw-r--r--pykolab/setup/setup_freebusy.py25
-rw-r--r--pykolab/setup/setup_mta.py17
-rw-r--r--pykolab/setup/setup_mysql.py4
-rw-r--r--pykolab/wap_client/__init__.py20
-rw-r--r--pykolab/xml/__init__.py18
-rw-r--r--pykolab/xml/attendee.py9
-rw-r--r--pykolab/xml/contact.py2
-rw-r--r--pykolab/xml/event.py50
-rw-r--r--pykolab/xml/recurrence_rule.py17
-rw-r--r--pykolab/xml/todo.py207
-rw-r--r--pykolab/xml/utils.py211
-rw-r--r--saslauthd/__init__.py17
-rw-r--r--saslauthd/kolab-saslauthd.sysvinit4
-rw-r--r--share/templates/roundcubemail/acl.inc.php.tpl3
-rw-r--r--share/templates/roundcubemail/calendar.inc.php.tpl4
-rw-r--r--share/templates/roundcubemail/config.inc.php.tpl3
-rw-r--r--share/templates/roundcubemail/kolab_files.inc.php.tpl2
-rw-r--r--share/templates/roundcubemail/kolab_folders.inc.php.tpl8
-rw-r--r--share/templates/roundcubemail/password.inc.php.tpl2
-rw-r--r--tests/functional/test_wallace/test_005_resource_invitation.py6
-rw-r--r--tests/functional/test_wallace/test_007_invitationpolicy.py289
-rw-r--r--tests/unit/test-003-event.py58
-rw-r--r--tests/unit/test-012-wallace_invitationpolicy.py32
-rw-r--r--tests/unit/test-016-todo.py239
-rw-r--r--wallace/module_invitationpolicy.py520
-rw-r--r--wallace/module_resources.py29
-rw-r--r--wallace/wallace.sysvinit2
40 files changed, 2257 insertions, 639 deletions
diff --git a/INSTALL b/INSTALL
index 21c764b..0f83cf9 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
* intltool
* rpm-build
-* python-icalendar
+* python-icalendar (version 3.8.2 or higher)
* python-kolabformat
* python-kolab
* python-nose
diff --git a/conf/kolab.conf b/conf/kolab.conf
index 627b23f..28aa1b3 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.
@@ -136,6 +140,9 @@ autocreate_folders = {
},
}
+[imap]
+virtual_domains = userid
+
[ldap]
; The URI to LDAP
ldap_uri = ldap://localhost:389
@@ -208,6 +215,9 @@ 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 --git a/configure.ac b/configure.ac
index 7ae2519..0aa71ff 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.1)
+AC_SUBST([RELEASE], 1)
AC_CONFIG_SRCDIR(pykolab/constants.py.in)
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/po/POTFILES.in b/po/POTFILES.in
index 5a5bc37..8afa8b9 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,8 @@ pykolab/xml/contact.py
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
@@ -168,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 1966fae..91d4f18 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,12 @@ 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/._todo.py
pykolab/xml/._utils.py
tests/functional/._purge_users.py
tests/functional/._resource_func.py
@@ -63,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 389ca9b..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-07-10 07:21-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@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@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:2137
+#: ../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,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: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:1356
+#: ../pykolab/auth/ldap/__init__.py:1359
#, python-format
msgid "Entry %s attribute value: %r"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:1364
+#: ../pykolab/auth/ldap/__init__.py:1367
#, 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:1687 ../pykolab/auth/ldap/__init__.py:1887
#, python-format
msgid "Result from recipient policy: %r"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:1908
+#: ../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:2067
+#: ../pykolab/auth/ldap/__init__.py:2101
#, python-format
msgid "Finding domain root dn for domain %s"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2164
+#: ../pykolab/auth/ldap/__init__.py:2198
msgid "Authentication database DOWN"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296
+#: ../pykolab/auth/ldap/__init__.py:2282 ../pykolab/auth/ldap/__init__.py:2330
#, 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:2419
msgid "LDAP Search Result Data Entry:"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2409
+#: ../pykolab/auth/ldap/__init__.py:2435
msgid "Entry Change Notification attributes:"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2414
+#: ../pykolab/auth/ldap/__init__.py:2440
#, python-format
msgid "Change Type: %r (%r)"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2422
+#: ../pykolab/auth/ldap/__init__.py:2448
#, python-format
msgid "Previous DN: %r"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2477
+#: ../pykolab/auth/ldap/__init__.py:2503
#, python-format
msgid "Object %s searched no longer exists"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2487
+#: ../pykolab/auth/ldap/__init__.py:2513
#, python-format
msgid "%d results..."
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2590
+#: ../pykolab/auth/ldap/__init__.py:2616
#, python-format
msgid "Searching with filter %r"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2642
+#: ../pykolab/auth/ldap/__init__.py:2668
#, python-format
msgid "Checking for support for %s on %s"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2661
+#: ../pykolab/auth/ldap/__init__.py:2687
#, python-format
msgid "Found support for %s"
msgstr ""
-#: ../pykolab/auth/ldap/__init__.py:2706
+#: ../pykolab/auth/ldap/__init__.py:2732
#, python-format
msgid "An error occured using %s: %r"
msgstr ""
+#: ../pykolab/auth/ldap/__init__.py:2738
+#, 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 ""
@@ -1484,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 ""
@@ -1538,84 +1544,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 +1685,194 @@ 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: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:453 ../pykolab/imap/__init__.py:675
+#: ../pykolab/imap/__init__.py:467 ../pykolab/imap/__init__.py:689
#, python-format
msgid "Downcasing mailbox name %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:457
+#: ../pykolab/imap/__init__.py:471
#, python-format
msgid "Creating new mailbox for user %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:470
+#: ../pykolab/imap/__init__.py:484
msgid "Waiting for the Cyrus IMAP Murder to settle..."
msgstr ""
-#: ../pykolab/imap/__init__.py:516
+#: ../pykolab/imap/__init__.py:530
#, python-format
msgid "Creating additional folders for user %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:535
+#: ../pykolab/imap/__init__.py:549
#, python-format
msgid "Waiting for the Cyrus murder to settle... %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:547
+#: ../pykolab/imap/__init__.py:561
#, python-format
msgid "Correcting additional folder name from %r to %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:553
+#: ../pykolab/imap/__init__.py:567
#, python-format
msgid "Mailbox already exists: %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:593
+#: ../pykolab/imap/__init__.py:607
msgid "Subscribing user to the additional folders"
msgstr ""
-#: ../pykolab/imap/__init__.py:607
+#: ../pykolab/imap/__init__.py:621
msgid "Using the following tests for folder subscriptions:"
msgstr ""
-#: ../pykolab/imap/__init__.py:609
+#: ../pykolab/imap/__init__.py:623
#, python-format
msgid " %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:612
+#: ../pykolab/imap/__init__.py:626
#, python-format
msgid "Folder %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:624
+#: ../pykolab/imap/__init__.py:638
#, python-format
msgid "Subscribing %s to folder %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:628
+#: ../pykolab/imap/__init__.py:642
#, python-format
msgid "Subscribing %s to folder %s failed: %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:658
+#: ../pykolab/imap/__init__.py:672
#, python-format
msgid "Could not rename %s to reside on partition %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:691
+#: ../pykolab/imap/__init__.py:705
#, 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:708 ../pykolab/imap/__init__.py:784
#, python-format
msgid "Renaming INBOX from %s to %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:698
+#: ../pykolab/imap/__init__.py:712
#, 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: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:704
+#: ../pykolab/imap/__init__.py:718
#, python-format
msgid "Server for mailbox %r is %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:712
+#: ../pykolab/imap/__init__.py:726
#, python-format
msgid "Looking for folder '%s', we found folders: %r"
msgstr ""
-#: ../pykolab/imap/__init__.py:735
+#: ../pykolab/imap/__init__.py:749
#, python-format
msgid "Setting ACL rights %s for subject %s on folder "
msgstr ""
-#: ../pykolab/imap/__init__.py:746
+#: ../pykolab/imap/__init__.py:760
#, python-format
msgid "Removing ACL rights %s for subject %s on folder "
msgstr ""
-#: ../pykolab/imap/__init__.py:767
+#: ../pykolab/imap/__init__.py:781
#, python-format
msgid "Found old INBOX folder %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:776
+#: ../pykolab/imap/__init__.py:790
#, python-format
msgid "Did not find old folder user/%s to rename"
msgstr ""
-#: ../pykolab/imap/__init__.py:778
+#: ../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:846
+#: ../pykolab/imap/__init__.py:860
#, python-format
msgid "Cannot get current IMAP quota for folder %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:859
+#: ../pykolab/imap/__init__.py:873
#, python-format
msgid "Quota for %s currently is %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:865
+#: ../pykolab/imap/__init__.py:879
#, python-format
msgid "Adjusting authentication database quota for folder %s to %d"
msgstr ""
-#: ../pykolab/imap/__init__.py:870
+#: ../pykolab/imap/__init__.py:884
#, python-format
msgid "Correcting quota for %s to %s (currently %s)"
msgstr ""
-#: ../pykolab/imap/__init__.py:947
+#: ../pykolab/imap/__init__.py:961
#, python-format
msgid "Checking folder: %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:952
+#: ../pykolab/imap/__init__.py:966
#, python-format
msgid "Folder has no corresponding user (1): %s"
msgstr ""
-#: ../pykolab/imap/__init__.py:955
+#: ../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:970
+#: ../pykolab/imap/__init__.py:984
msgid "Please don't give us just a user identifier"
msgstr ""
-#: ../pykolab/imap/__init__.py:973
+#: ../pykolab/imap/__init__.py:987
#, python-format
msgid "Deleting folder %s"
msgstr ""
@@ -1865,50 +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:225
+#: ../pykolab/itip/__init__.py:221
+#, python-format
+msgid "Send iTip reply %s for %s %r"
+msgstr ""
+
+#: ../pykolab/itip/__init__.py:237
#, python-format
-msgid "Failed to compose iTip reply message: %r"
+msgid "Failed to compose iTip reply message: %r: %s"
msgstr ""
-#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936
-#: ../wallace/module_resources.py:964
+#: ../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/logger.py:173 ../pykolab/logger.py:179
+#: ../pykolab/itip/__init__.py:280
+#, python-format
+msgid "Failed to compose iTip request message: %r"
+msgstr ""
+
+#: ../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 ""
@@ -2043,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 ""
@@ -2087,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"
@@ -2108,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 ""
@@ -2407,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 ""
@@ -2442,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 ""
@@ -2477,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 ""
@@ -2548,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 ""
@@ -2645,18 +2673,18 @@ msgstr ""
msgid "Could not translate %s using locale %s"
msgstr ""
-#: ../pykolab/wap_client/__init__.py:320
+#: ../pykolab/wap_client/__init__.py:396
#, python-format
msgid "Requesting %r with params %r"
msgstr ""
-#: ../pykolab/wap_client/__init__.py:328
+#: ../pykolab/wap_client/__init__.py:404
#, python-format
msgid "Got response: %r"
msgstr ""
#. Some data is not JSON
-#: ../pykolab/wap_client/__init__.py:334
+#: ../pykolab/wap_client/__init__.py:410
msgid "Response data is not JSON"
msgstr ""
@@ -2681,137 +2709,324 @@ 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:108 ../pykolab/xml/attendee.py:130
+#: ../pykolab/xml/attendee.py:132 ../pykolab/xml/attendee.py:154
msgid "Not a valid attendee"
msgstr ""
-#: ../pykolab/xml/attendee.py:115
+#: ../pykolab/xml/attendee.py:139
msgid "No valid delegator references found"
msgstr ""
-#: ../pykolab/xml/attendee.py:135
+#: ../pykolab/xml/attendee.py:159
msgid "No valid delegatee references found"
msgstr ""
-#: ../pykolab/xml/attendee.py:180
+#: ../pykolab/xml/attendee.py:219
#, python-format
msgid "Invalid cutype %r"
msgstr ""
-#: ../pykolab/xml/attendee.py:192
+#: ../pykolab/xml/attendee.py:231
#, python-format
msgid "Invalid participant status %r"
msgstr ""
-#: ../pykolab/xml/attendee.py:200
+#: ../pykolab/xml/attendee.py:239
#, 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: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:241
+#: ../pykolab/xml/event.py:294
#, python-format
msgid "No attendee with email or name %r"
msgstr ""
-#: ../pykolab/xml/event.py:249
+#: ../pykolab/xml/event.py:302
#, python-format
msgid "Invalid argument value attendee %r, must be basestring or Attendee"
msgstr ""
-#: ../pykolab/xml/event.py:255
+#: ../pykolab/xml/event.py:314
#, python-format
msgid "No attendee with email %r"
msgstr ""
-#: ../pykolab/xml/event.py:261
+#: ../pykolab/xml/event.py:320
#, python-format
msgid "No attendee with name %r"
msgstr ""
-#: ../pykolab/xml/event.py:426
-msgid "Invalid participant status"
+#: ../pykolab/xml/event.py:370 ../pykolab/xml/utils.py:151
+msgid "%Y-%m-%d"
msgstr ""
-#: ../pykolab/xml/event.py:542
-#, python-format
-msgid "Invalid status %r"
+#: ../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:550
+#: ../pykolab/xml/event.py:618
#, python-format
msgid "Invalid classification %r"
msgstr ""
-#: ../pykolab/xml/event.py:577
+#: ../pykolab/xml/event.py:649
msgid "Event end needs datetime.date or datetime.datetime instance"
msgstr ""
-#: ../pykolab/xml/event.py:761
+#: ../pykolab/xml/event.py:659
+#, python-format
+msgid "Invalid custom property name %r"
+msgstr ""
+
+#: ../pykolab/xml/event.py:837
#, python-format
msgid "Invalid status set: %r"
msgstr ""
-#: ../pykolab/xml/event.py:923
+#: ../pykolab/xml/event.py:1074
msgid "No sender specified"
msgstr ""
-#: ../pykolab/xml/event.py:932
+#: ../pykolab/xml/event.py:1083
#, python-format
msgid "Invitation for %s was %s"
msgstr ""
-#: ../pykolab/xml/event.py:937
+#: ../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:190
-#: ../wallace/module_resources.py:879
+#: ../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: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: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 ""
-#: ../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: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:240
+#: ../wallace/module_invitationpolicy.py:441
#, python-format
msgid "\"%(summary)s\" has been %(status)s"
msgstr ""
@@ -2819,19 +3034,37 @@ 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
+#. 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:627
-#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639
+#: ../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"
+msgstr ""
+
#: ../wallace/__init__.py:57
#, python-format
msgid "Wallace modules: %r"
@@ -2874,22 +3107,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: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:170 ../wallace/module_optout.py:62
-#: ../wallace/module_resources.py:126
+#: ../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:172 ../wallace/module_optout.py:64
-#: ../wallace/module_resources.py:129
+#: ../wallace/module_invitationpolicy.py:214 ../wallace/module_optout.py:64
+#: ../wallace/module_resources.py:134
#, python-format
msgid "Attempting to execute cb_action_%s()"
msgstr ""
@@ -2954,242 +3187,286 @@ msgstr ""
msgid "An error occurred: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:154
+#: ../wallace/module_invitationpolicy.py:196
#, python-format
msgid "Invitation policy called for %r, %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:211
-#: ../wallace/module_resources.py:169
+#: ../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:215
+#: ../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:219
+#: ../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:232
+#: ../wallace/module_invitationpolicy.py:278
#, python-format
msgid "No itips, no users, pass along %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:235
+#: ../wallace/module_invitationpolicy.py:281
#, python-format
msgid "iTips, but no users, pass along %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:255
+#: ../wallace/module_invitationpolicy.py:301
#, python-format
msgid "No user attendee matching envelope recipient %s, skip message"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:259
+#: ../wallace/module_invitationpolicy.py:305
#, python-format
msgid "Receiving user: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:284
+#: ../wallace/module_invitationpolicy.py:330
#, 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:341
#, python-format
msgid "Ignoring '%s' iTip method"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:299
+#: ../wallace/module_invitationpolicy.py:345
#, python-format
msgid "iTip message %r consumed by the invitationpolicy module"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:315
+#: ../wallace/module_invitationpolicy.py:361
msgid "Pass invitation for manual processing"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:320
+#: ../wallace/module_invitationpolicy.py:366
#, python-format
msgid "Receiving Attendee: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:339
+#: ../wallace/module_invitationpolicy.py:386
#, python-format
-msgid "Existing event: %r"
+msgid "Existing %s: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:350
+#: ../wallace/module_invitationpolicy.py:397
#, python-format
-msgid "Precondition for event %r fulfilled: %r"
+msgid "Precondition for object %r fulfilled: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:386
+#: ../wallace/module_invitationpolicy.py:415
#, python-format
-msgid "No RSVP for recipient %r requested"
+msgid ""
+"The iTip request sequence (%r) doesn't match the referred object version (%"
+"r). Ignoring."
msgstr ""
-#: ../wallace/module_invitationpolicy.py:412
+#: ../wallace/module_invitationpolicy.py:420
+#, python-format
+msgid "Auto-updating %s %r on iTip REQUEST (no re-scheduling)"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:475
msgid "Pass reply for manual processing"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:419
+#: ../wallace/module_invitationpolicy.py:482
#, python-format
msgid "Sender Attendee: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:431
+#: ../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:437
+#: ../wallace/module_invitationpolicy.py:500
+#, python-format
+msgid "Auto-updating %s %r on iTip REPLY"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:525
#, python-format
-msgid "Auto-updating event %r on iTip REPLY"
+msgid "Add delegatee: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:459
-#: ../wallace/module_invitationpolicy.py:488
+#: ../wallace/module_invitationpolicy.py:528
+#, python-format
+msgid "Update existing delegatee: %r"
+msgstr ""
+
+#: ../wallace/module_invitationpolicy.py:533
+#, python-format
+msgid "Update delegator: %r"
+msgstr ""
+
+#: ../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:472
+#: ../wallace/module_invitationpolicy.py:563
msgid "Pass cancellation for manual processing"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:517
+#: ../wallace/module_invitationpolicy.py:611
#, python-format
msgid "Checking if email address %r belongs to a local user"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:522
+#: ../wallace/module_invitationpolicy.py:616
#, python-format
msgid "User DN: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:524
+#: ../wallace/module_invitationpolicy.py:618
#, python-format
msgid "No user record(s) found for %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:577
+#: ../wallace/module_invitationpolicy.py:674
#, python-format
msgid "User record doesn't have the mailbox attribute %r set"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:590
+#: ../wallace/module_invitationpolicy.py:687
#, python-format
msgid "IMAP proxy authentication failed: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:612
+#: ../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:628
+#: ../wallace/module_invitationpolicy.py:725
#, python-format
msgid "IMAP metadata for %r: %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:658
+#: ../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:670
-#: ../wallace/module_invitationpolicy.py:709
-#: ../wallace/module_resources.py:486
+#: ../wallace/module_invitationpolicy.py:771
#, python-format
-msgid "Failed to parse event from message %s/%s: %r"
+msgid "Failed to parse %s from message %s/%s: %s"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:696
+#: ../wallace/module_invitationpolicy.py:797
#, python-format
msgid "Listing events from folder %r"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:715
+#: ../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:816
#, 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:823
+#: ../wallace/module_resources.py:419
#, python-format
msgid "start: %r, end: %r, total: %r, messages: %d"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:748
+#: ../wallace/module_invitationpolicy.py:849
#, python-format
msgid "%r is locked, waiting..."
msgstr ""
-#: ../wallace/module_invitationpolicy.py:811
+#: ../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:814
+#: ../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:827
+#: ../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:843
+#: ../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:863
+#: ../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:901
+#: ../wallace/module_invitationpolicy.py:1003
#, python-format
msgid ""
"Waiting for more automated replies (got %d of %d); skipping notification"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:998
+#: ../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:1001
+#: ../wallace/module_invitationpolicy.py:1186
#, python-format
msgid "Attendee %s's copy of %r not found"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:1004
+#: ../wallace/module_invitationpolicy.py:1189
#, python-format
msgid "Attendee %r not found in LDAP"
msgstr ""
-#: ../wallace/module_invitationpolicy.py:1008
+#: ../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')
@@ -3213,184 +3490,230 @@ 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: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:182
+#: ../wallace/module_resources.py:193
msgid "iTip events attached to this message contain the "
msgstr ""
-#: ../wallace/module_resources.py:205
+#: ../wallace/module_resources.py:226
msgid "Not an iTip message, but sent to resource nonetheless. Reject message"
msgstr ""
-#: ../wallace/module_resources.py:213
+#: ../wallace/module_resources.py:234
#, python-format
msgid "No itips, no resources, pass along %r"
msgstr ""
-#: ../wallace/module_resources.py:216
+#: ../wallace/module_resources.py:237
#, python-format
msgid "iTips, but no resources, pass along %r"
msgstr ""
-#: ../wallace/module_resources.py:225
+#: ../wallace/module_resources.py:246
#, python-format
msgid "No resource attendees matching envelope recipient %s, Reject message"
msgstr ""
-#: ../wallace/module_resources.py:234
+#: ../wallace/module_resources.py:256
#, python-format
msgid "Resources: %r; %r"
msgstr ""
-#: ../wallace/module_resources.py:244
+#: ../wallace/module_resources.py:274
+#, python-format
+msgid "Sender Attendee: %r => %r"
+msgstr ""
+
+#: ../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:306
+#, python-format
+msgid "Event referenced by this REPLY (%r) not found in resource calendar"
+msgstr ""
+
+#: ../wallace/module_resources.py:309
+msgid "No event reference found in this REPLY. Ignoring."
+msgstr ""
+
+#: ../wallace/module_resources.py:318
#, python-format
msgid "Receiving Resource: %r; %r"
msgstr ""
-#: ../wallace/module_resources.py:252
+#: ../wallace/module_resources.py:326
#, python-format
msgid "Recipient %r is non-participant, ignoring message"
msgstr ""
-#: ../wallace/module_resources.py:279
+#: ../wallace/module_resources.py:354
#, python-format
msgid "Accept invitation for individual resource %r / %r"
msgstr ""
-#: ../wallace/module_resources.py:308
+#: ../wallace/module_resources.py:383
#, python-format
msgid "Delegate invitation for resource collection %r to %r"
msgstr ""
-#: ../wallace/module_resources.py:340
+#: ../wallace/module_resources.py:415
#, python-format
msgid "Failed to read resource calendar for %r: %r"
msgstr ""
-#: ../wallace/module_resources.py:350
+#: ../wallace/module_resources.py:425
#, python-format
msgid "Polling for resource %r"
msgstr ""
-#: ../wallace/module_resources.py:353
+#: ../wallace/module_resources.py:428
#, python-format
msgid "Resource %r has been popped from the list"
msgstr ""
-#: ../wallace/module_resources.py:357
+#: ../wallace/module_resources.py:432
msgid "Resource is a collection"
msgstr ""
-#: ../wallace/module_resources.py:368
+#: ../wallace/module_resources.py:443
#, python-format
msgid "Removed conflicting resources from %r: (%r) => %r"
msgstr ""
-#: ../wallace/module_resources.py:380
+#: ../wallace/module_resources.py:455
#, python-format
msgid "Conflicting events: %r for resource %r"
msgstr ""
-#: ../wallace/module_resources.py:397
+#: ../wallace/module_resources.py:474
#, python-format
msgid "Delegate to another resource collection member: %r to %r"
msgstr ""
-#: ../wallace/module_resources.py:459
+#: ../wallace/module_resources.py:536
#, python-format
msgid "Checking events in resource folder %r"
msgstr ""
-#: ../wallace/module_resources.py:475
+#: ../wallace/module_resources.py:555
#, python-format
msgid "Fetching message UID %r from folder %r"
msgstr ""
-#: ../wallace/module_resources.py:498
+#: ../wallace/module_resources.py:578
#, python-format
msgid "Event %r conflicts with event %r"
msgstr ""
-#: ../wallace/module_resources.py:525
+#: ../wallace/module_resources.py:599
+#, python-format
+msgid "Searching %r for event %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:605
+#, python-format
+msgid "Failed to access resource calendar:: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:634
+#, python-format
+msgid "Apply invitation policies %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:653
#, python-format
msgid "Adding event to %r: %r"
msgstr ""
-#: ../wallace/module_resources.py:573
+#: ../wallace/module_resources.py:707
#, python-format
msgid "Failed to save event to resource calendar at %r: %r"
msgstr ""
-#: ../wallace/module_resources.py:590
+#: ../wallace/module_resources.py:724
#, python-format
msgid "Delete resource calendar object %r in %r: %r"
msgstr ""
-#: ../wallace/module_resources.py:633
+#: ../wallace/module_resources.py:767
#, 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:775 ../wallace/module_resources.py:849
+#: ../wallace/module_resources.py:883
#, 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: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:647 ../wallace/module_resources.py:715
-#: ../wallace/module_resources.py:750
+#: ../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:667
+#: ../wallace/module_resources.py:801
#, python-format
msgid "Raw itip_events: %r"
msgstr ""
-#: ../wallace/module_resources.py:675
+#: ../wallace/module_resources.py:809
#, python-format
msgid "Raw set of attendees: %r"
msgstr ""
-#: ../wallace/module_resources.py:683
+#: ../wallace/module_resources.py:817
#, python-format
msgid "Raw set of resources: %r"
msgstr ""
-#: ../wallace/module_resources.py:702
+#: ../wallace/module_resources.py:822
+#, python-format
+msgid "Raw set of organizers: %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:842
#, 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:858 ../wallace/module_resources.py:892
msgid "Resource reservation made but no resource records found"
msgstr ""
-#: ../wallace/module_resources.py:737
+#: ../wallace/module_resources.py:877
#, python-format
msgid "Checking if resource %r is a resource (collection)"
msgstr ""
-#: ../wallace/module_resources.py:755
+#: ../wallace/module_resources.py:895
msgid "The following resources are being referred to in the "
msgstr ""
-#: ../wallace/module_resources.py:894
+#: ../wallace/module_resources.py:1060
#, python-format
msgid ""
"\n"
@@ -3401,7 +3724,7 @@ msgid ""
" "
msgstr ""
-#: ../wallace/module_resources.py:905
+#: ../wallace/module_resources.py:1079
#, python-format
msgid ""
"\n"
@@ -3411,7 +3734,7 @@ msgid ""
" "
msgstr ""
-#: ../wallace/module_resources.py:912
+#: ../wallace/module_resources.py:1086
#, python-format
msgid ""
"\n"
@@ -3420,16 +3743,16 @@ msgid ""
" "
msgstr ""
-#: ../wallace/module_resources.py:941
+#: ../wallace/module_resources.py:1117
#, python-format
msgid "Sending booking notification for event %r to %r from %r"
msgstr ""
-#: ../wallace/module_resources.py:954
+#: ../wallace/module_resources.py:1134
msgid "failed"
msgstr ""
-#: ../wallace/module_resources.py:973
+#: ../wallace/module_resources.py:1153
#, python-format
msgid ""
"\n"
@@ -3441,7 +3764,7 @@ msgid ""
" "
msgstr ""
-#: ../wallace/module_resources.py:979
+#: ../wallace/module_resources.py:1159
#, python-format
msgid ""
"\n"
@@ -3455,6 +3778,29 @@ msgid ""
" "
msgstr ""
+#: ../wallace/module_resources.py:1203
+#, python-format
+msgid "Clone invitation for owner confirmation: %r from %r"
+msgstr ""
+
+#: ../wallace/module_resources.py:1209
+#, 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
diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index f15d2c8..b9d0749 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'
@@ -1232,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)
@@ -1257,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:
@@ -1272,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:
@@ -1593,6 +1595,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'
@@ -1611,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)
@@ -1636,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:
@@ -1644,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:
@@ -1653,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:
@@ -1773,7 +1775,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'
@@ -1802,6 +1804,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)
diff --git a/pykolab/conf/__init__.py b/pykolab/conf/__init__.py
index 8b9f444..1af4048 100644
--- a/pykolab/conf/__init__.py
+++ b/pykolab/conf/__init__.py
@@ -606,6 +606,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/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
diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index f52dc9f..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',
@@ -364,7 +366,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):
"""
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/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:
diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py
index 6993d5e..05309a3 100644
--- a/pykolab/setup/setup_freebusy.py
+++ b/pykolab/setup/setup_freebusy.py
@@ -88,21 +88,34 @@ 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 "kolab-users"', {
+ ('directory "local"': {
+ 'type': 'static',
+ 'fbsource': 'file:/var/lib/kolab-freebusy/%s.ifb',
+ }),
+ ('directory "local-cache"': {
+ 'type': 'static',
+ 'fbsource': 'file:/var/cache/kolab-freebusy/%s.ifb',
+ 'expires': '15m'
+ }),
+ ('directory "kolab-people"': {
'type': 'ldap',
'host': conf.get('ldap', 'ldap_uri'),
'base_dn': conf.get('ldap', 'base_dn'),
'bind_dn': conf.get('ldap', 'service_bind_dn'),
'bind_pw': conf.get('ldap', 'service_bind_pw'),
- 'filter': '(&(objectClass=kolabInetOrgPerson)(|(uid=%s)(mail=%s)(alias=%s)))',
+ 'filter': '(&(objectClass=kolabInetOrgPerson)(|(mail=%s)(alias=%s)))',
'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,
}),
@@ -113,9 +126,9 @@ 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',
+ 'cacheto': '/var/cache/kolab-freebusy/%s.ifb',
'expires': '15m',
'loglevel': 300
}),
diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
index c72b18a..73c21a8 100644
--- a/pykolab/setup/setup_mta.py
+++ b/pykolab/setup/setup_mta.py
@@ -295,14 +295,19 @@ def _execute(*args, **kw):
'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:
@@ -353,7 +358,7 @@ def _execute(*args, **kw):
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@amavisd'
if not (
control_service('postfix', 'restart') and
@@ -387,7 +392,7 @@ _postfix_main_settings = {
"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",
@@ -535,7 +540,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>
"""
diff --git a/pykolab/setup/setup_mysql.py b/pykolab/setup/setup_mysql.py
index 377a3aa..241c4e8 100644
--- a/pykolab/setup/setup_mysql.py
+++ b/pykolab/setup/setup_mysql.py
@@ -176,6 +176,7 @@ def _execute(*args, **kw):
# 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:
@@ -208,6 +209,9 @@ def _execute(*args, **kw):
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)
+ 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:
diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index 7b2b565..68f2a7c 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 is None:
if API_SSL:
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 3e12716..2c99717 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -13,6 +13,15 @@ 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 property_label
+from utils import property_to_string
+from utils import compute_diff
from utils import to_dt
__all__ = [
@@ -20,9 +29,17 @@ __all__ = [
"Contact",
"ContactReference",
"Event",
+ "Todo",
"RecurrenceRule",
"event_from_ical",
"event_from_string",
+ "event_from_message",
+ "todo_from_ical",
+ "todo_from_string",
+ "todo_from_message",
+ "property_label",
+ "property_to_string",
+ "compute_diff",
"to_dt",
]
@@ -30,6 +47,7 @@ errors = [
"EventIntegrityError",
"InvalidEventDateError",
"InvalidAttendeeParticipantStatusError",
+ "TodoIntegrityError",
]
__all__.extend(errors)
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index a6384e9..10fd006 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -12,13 +12,15 @@ 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"),
+ kolabformat.PartCompleted: N_("Completed"),
+ kolabformat.PartInProcess: N_("Started"),
}
def participant_status_label(status):
@@ -38,9 +40,8 @@ class Attendee(kolabformat.Attendee):
"DECLINED": kolabformat.PartDeclined,
"TENTATIVE": kolabformat.PartTentative,
"DELEGATED": kolabformat.PartDelegated,
- # Not yet implemented
- #"COMPLETED": ,
- #"IN-PROCESS": ,
+ "COMPLETED": kolabformat.PartCompleted,
+ "IN-PROCESS": kolabformat.PartInProcess,
}
# See RFC 2445, 5445
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 c199a5a..7b9e13c 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
@@ -45,10 +43,15 @@ def event_from_message(message):
class Event(object):
+ type = 'event'
+
status_map = {
"TENTATIVE": kolabformat.StatusTentative,
"CONFIRMED": kolabformat.StatusConfirmed,
"CANCELLED": kolabformat.StatusCancelled,
+ "COMPLETD": kolabformat.StatusCompleted,
+ "IN-PROCESS": kolabformat.StatusInProcess,
+ "NEEDS-ACTION": kolabformat.StatusNeedsAction,
}
classification_map = {
@@ -362,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')
@@ -611,9 +619,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))
@@ -623,7 +631,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
@@ -655,20 +663,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 +735,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]
@@ -774,12 +778,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))
@@ -941,7 +945,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/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/todo.py b/pykolab/xml/todo.py
new file mode 100644
index 0000000..0d34c63
--- /dev/null
+++ b/pykolab/xml/todo.py
@@ -0,0 +1,207 @@
+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):
+ type = 'task'
+
+ 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 set_transparency(self, transp):
+ # empty stub
+ pass
+
+ 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/pykolab/xml/utils.py b/pykolab/xml/utils.py
index 2fddb24..2fe82d2 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -2,6 +2,11 @@ import datetime
import pytz
import kolabformat
from dateutil.tz import tzlocal
+from collections import OrderedDict
+
+from pykolab.translate import _
+from pykolab.translate import N_
+
def to_dt(dt):
"""
@@ -62,10 +67,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,
@@ -92,6 +105,200 @@ 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__())
+
+ if as_utc:
+ _cdatetime.setUTC(True)
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
+ """
+ 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 compare_values(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 compare_values(aa, bb):
+ if reduced:
+ (old, new) = reduce_properties(aa, bb)
+ else:
+ (old, new) = (aa, bb)
+ diff.append(OrderedDict([('property', prop), ('old', old), ('new', new)]))
+
+ return diff
+
+
+def 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
+ """
+ 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/saslauthd/__init__.py b/saslauthd/__init__.py
index 32927a8..6590747 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",
@@ -161,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)
@@ -262,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
)
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
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__));
}
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';
diff --git a/share/templates/roundcubemail/config.inc.php.tpl b/share/templates/roundcubemail/config.inc.php.tpl
index da80933..0570ea4 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'] = '';
@@ -57,6 +59,7 @@
'kolab_files',
'kolab_folders',
'kolab_notes',
+ 'kolab_tags',
'libkolab',
'libcalendaring',
'managesieve',
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');
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';
diff --git a/share/templates/roundcubemail/password.inc.php.tpl b/share/templates/roundcubemail/password.inc.php.tpl
index 063156c..02f7bd2 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.
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 0f05993..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
@@ -323,7 +326,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/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index bdbfc98..c3be462 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@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@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@example.org',
- 'kolabtargetfolder': 'user/john.doe/Calendar@example.org',
+ 'kolabcalendarfolder': 'user/john.doe/Calendar@example.org',
+ 'kolabtasksfolder': 'user/john.doe/Tasks@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@example.org',
- 'kolabtargetfolder': 'user/jane.manager/Calendar@example.org',
- 'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','ACT_UPDATE']
+ 'kolabcalendarfolder': 'user/jane.manager/Calendar@example.org',
+ 'kolabtasksfolder': 'user/jane.manager/Tasks@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@example.org',
- 'kolabtargetfolder': 'user/jack.tentative/Calendar@example.org',
- 'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
+ 'kolabcalendarfolder': 'user/jack.tentative/Calendar@example.org',
+ 'kolabtasksfolder': 'user/jack.tentative/Tasks@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@example.org',
- 'kolabtargetfolder': 'user/mark.german/Calendar@example.org',
+ 'kolabcalendarfolder': 'user/mark.german/Calendar@example.org',
+ 'kolabtasksfolder': 'user/mark.german/Tasks@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,
@@ -310,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)
@@ -342,7 +421,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
imap = IMAP()
imap.connect()
- mailbox = imap.folder_quote(user['kolabtargetfolder'])
+ mailbox = imap.folder_quote(folder)
imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
imap.imap.m.select(mailbox)
@@ -355,11 +434,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 +484,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 +529,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 +545,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 +588,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 +596,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 +609,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 +635,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 +650,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 +663,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 +671,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 +688,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 +704,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 +712,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 +731,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 +739,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 +766,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 +843,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 +855,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 +865,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 +873,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 +893,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 +901,105 @@ 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_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@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)
+
+ 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_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"))
+ 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 = '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
+ 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_022_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-003-event.py b/tests/unit/test-003-event.py
index d9e05fa..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
@@ -14,6 +15,9 @@ 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 pykolab.xml import property_to_string
+from collections import OrderedDict
ical_event = """
BEGIN:VEVENT
@@ -223,7 +227,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>
@@ -245,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)
@@ -615,6 +630,47 @@ 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@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 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@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:
+ return prop
+ return None
+
if __name__ == '__main__':
unittest.main()
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@fastmail.net'), [MIP.ACT_MANUAL])
- self.assertEqual(MIP.get_matching_invitation_policies(user, 'b@example.org'), [MIP.ACT_ACCEPT,MIP.ACT_MANUAL])
- self.assertEqual(MIP.get_matching_invitation_policies(user, 'c@gmail.com'), [MIP.ACT_REJECT,MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'a@fastmail.net', MIP.COND_TYPE_EVENT), [MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'b@example.org', MIP.COND_TYPE_EVENT), [MIP.ACT_ACCEPT, MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'c@gmail.com', MIP.COND_TYPE_EVENT), [MIP.ACT_REJECT, MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'd@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@somedomain.net'), [MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'd@somedomain.net', MIP.COND_TYPE_ALL), [MIP.ACT_MANUAL])
def test_004_write_locks(self):
user = { 'cn': 'John Doe', 'mail': "doe@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@domain.org'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'user@domain.org'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'user@domain.com'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'user@domain.com'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'user@domain.com'))
- self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'sam@example.org'))
- self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user@domain.com'))
- self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john@example.org'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual }, 'user@domain.org', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'user@domain.org', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'user@domain.com', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'user@domain.com', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'user@domain.com', 'event'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'sam@example.org', 'event'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user@domain.com', 'event'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john@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
new file mode 100644
index 0000000..c6a1178
--- /dev/null
+++ b/tests/unit/test-016-todo.py
@@ -0,0 +1,239 @@
+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
+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@example.org
+ORGANIZER;CN=Thomas:mailto:thomas.bruederli@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@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
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 8e77335..5c187c5 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,30 +41,33 @@ from pykolab.auth import Auth
from pykolab.conf import Conf
from pykolab.imap import IMAP
from pykolab.xml import to_dt
+from pykolab.xml import utils as xmlutils
+from pykolab.xml import todo_from_message
from pykolab.xml import event_from_message
from pykolab.xml import participant_status_label
-from pykolab.itip import 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 +75,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()
@@ -200,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]
@@ -210,17 +252,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 +281,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 +309,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 +369,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:
@@ -364,6 +408,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, existing, False)
+
# if RSVP, send an iTip REPLY
if rsvp or scheduling_required:
# set attendee's CN from LDAP record if yet missing
@@ -373,33 +437,29 @@ 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
return None
- else:
- log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
- # TODO: only update if policy & ACT_UPDATE ?
-
- if save_event:
+ 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 +486,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 +535,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)
+ send_update_notification(existing, receiving_user, existing, True)
# 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 +565,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 +625,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 +646,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 +660,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 +690,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 +724,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 +751,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 +760,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 +793,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 +880,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 +921,29 @@ def store_event(event, user_rec, targetfolder=None):
imap.folder_utf7(targetfolder),
None,
None,
- event.to_message(creator="Kolab Server <wallace@localhost>").as_string()
+ object.to_message(creator="Kolab Server <wallace@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 +952,7 @@ def delete_event(existing):
imap.imap.m.expunge()
-def send_reply_notification(event, receiving_user):
+def send_update_notification(object, receiving_user, old=None, reply=True):
"""
Send a (consolidated) notification about the current participant status to organizer
"""
@@ -891,68 +962,152 @@ 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']
+ organizer = object.get_organizer()
+ orgemail = organizer.email()
+ orgname = organizer.name()
+
+ if reply:
+ log.debug(_("Compose participation status summary for %s %r to user %r") % (
+ object.type, object.uid, receiving_user['mail']
+ ), level=8)
+
+ auto_replies_expected = 0
+ auto_replies_received = 0
+ 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())
+
+ # 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:
+ roundup = "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
+
+ # list properties changed from previous version
+ if old:
+ diff = xmlutils.compute_diff(old.to_dict(), object.to_dict())
+ if len(diff) > 1:
+ roundup += "\n"
+ for change in diff:
+ if not change['property'] in ['created','lastmodified-date','sequence']:
+ new_value = xmlutils.property_to_string(change['property'], change['new']) if change['new'] else _("(removed)")
+ if new_value:
+ roundup += "\n- %s: %s" % (xmlutils.property_label(change['property']), new_value)
+
+ # compose different notification texts for events/tasks
+ if object.type == 'task':
+ message_text = """
+ The assignment for '%(summary)s' has been updated in your tasklist.
+ %(roundup)s
+ """ % {
+ 'summary': object.get_summary(),
+ 'roundup': roundup
+ }
+ else:
+ message_text = """
+ The event '%(summary)s' at %(start)s has been updated in your calendar.
+ %(roundup)s
+ """ % {
+ 'summary': object.get_summary(),
+ 'start': xmlutils.property_to_string('start', object.get_start()),
+ 'roundup': roundup
+ }
+
+ 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') % (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 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 = 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():
- parstat = attendee.get_participant_status(True)
- if partstats.has_key(parstat):
- partstats[parstat].append(attendee.get_displayname())
- else:
- partstats['PENDING'].append(attendee.get_displayname())
+ # 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': xmlutils.property_to_string('start', object.get_start()),
+ 'organizer': orgname if orgname else orgemail
+ }
- # 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):
- 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"
-
- 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
- }
+ 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 cancelled') % (object.get_summary())
msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
smtp = smtplib.SMTP("localhost", 10027)
@@ -968,10 +1123,10 @@ def send_reply_notification(event, receiving_user):
smtp.quit()
-def is_auto_reply(user, sender_email):
+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 +1138,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 +1153,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
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f38ae31..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]
@@ -205,10 +209,13 @@ def execute(*args, **kw):
for recipient in recipients:
# extract reference UID from recipients like resource+UID@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
@@ -321,7 +328,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 +454,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
@@ -527,6 +537,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')
@@ -616,7 +629,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)
@@ -680,7 +693,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,
@@ -703,7 +716,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)
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