diff options
40 files changed, 8796 insertions, 2264 deletions
diff --git a/conf/kolab.conf b/conf/kolab.conf index 6bf2125..627b23f 100644 --- a/conf/kolab.conf +++ b/conf/kolab.conf @@ -235,6 +235,10 @@ domain_rootdn_attribute = inetdomainbasedn ; The attribute that holds the quota. quota_attribute = mailquota + +; The format of the modifytimestamp attribute values +modifytimestamp_format = %Y%m%d%H%M%SZ + ; A unique attribute that can be used to identify the entry beyond renames and ; moves. Note that 'nsuniqueid' is specific to all Netscape-based directory ; services. @@ -374,7 +378,7 @@ mail_gid = vmail mail_location = mbox:/var/mail/vmail/%%u [wallace] -modules = resources, footer +modules = resources, invitationpolicy, footer footer_text = /etc/kolab/footer.text footer_html = /etc/kolab/footer.html @@ -386,6 +390,9 @@ bind_proxy_uri = ; This will normally be localhost. host = +; default settings for kolabInvitationPolicy +kolab_invitation_policy = ACT_ACCEPT_IF_NO_CONFLICT:example.org, ACT_MANUAL + ; This is a domain name space specific section, that enables us to override ; all settings, for example, the LDAP URI, base and bind DNs, scopes, filters, ; etc. Note that overriding the LDAP settings for the primary domain name space diff --git a/po/POTFILES.in b/po/POTFILES.in index 361bce9..5a5bc37 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -12,10 +12,10 @@ ext/python/Tools/freeze/makefreeze.py ext/python/Tools/freeze/makemakefile.py ext/python/Tools/freeze/parsesetup.py ext/python/Tools/freeze/winmakemakefile.py +kolab-cli.py kolabd/__init__.py kolabd/process.py kolabd.py -kolab-cli.py pykolab/auth/__init__.py pykolab/auth/ldap/auth_cache.py pykolab/auth/ldap/cache.py @@ -83,11 +83,13 @@ pykolab/imap/cyrus.py pykolab/imap/__init__.py pykolab/imap_utf7.py pykolab/__init__.py +pykolab/itip/__init__.py pykolab/logger.py pykolab/plugins/defaultfolders/__init__.py pykolab/plugins/dynamicquota/__init__.py pykolab/plugins/__init__.py pykolab/plugins/recipientpolicy/__init__.py +pykolab/plugins/roundcubedb/__init__.py pykolab/plugins/sievemgmt/__init__.py pykolab/setup/components.py pykolab/setup/__init__.py @@ -136,6 +138,8 @@ tests/functional/test_wallace/test_003_nonascii_subject.py tests/functional/test_wallace/test_004_nonascii_addresses.py tests/functional/test_wallace/test_005_resource_add.py tests/functional/test_wallace/test_005_resource_invitation.py +tests/functional/test_wallace/test_006_resource_performance.py +tests/functional/test_wallace/test_007_invitationpolicy.py tests/functional/test_wap_client/__init__.py tests/functional/test_wap_client/test_001_connect.py tests/functional/test_wap_client/test_002_user_add.py @@ -159,12 +163,18 @@ tests/unit/test-007-ldap_syncrepl.py tests/unit/test-008-sievelib.py tests/unit/test-009-parse_ldap_uri.py tests/unit/test-010-transliterate.py +tests/unit/test-011-itip.py tests/unit/test-011-wallace_resources.py +tests/unit/test-012-wallace_invitationpolicy.py +tests/unit/test-014-conf-and-raw.py +tests/unit/test-015-translate.py test-wallace.py ucs/kolab_sieve.py ucs/listener.py wallace/__init__.py wallace/module_footer.py +wallace/module_gpgencrypt.py +wallace/module_invitationpolicy.py wallace/module_optout.py wallace/module_resources.py wallace/modules.py diff --git a/po/POTFILES.skip b/po/POTFILES.skip index b2ac963..1966fae 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -1,214 +1,70 @@ -activate_domain.py -bin/test_parse_policy.py -bin/test-read-input.py -bonnie/__init__.py -bonnie.py -demo.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/about.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/authentication.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/data.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/environ.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/error.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/index.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/login.mak.py -kwap/data/templates/home/kanarip/devel/src/kolab/pykolab.git/kwap/kwap/templates/master.mak.py -kwap/ez_setup/__init__.py -kwap/kwap/config/app_cfg.py -kwap/kwap/config/environment.py -kwap/kwap/config/__init__.py -kwap/kwap/config/middleware.py -kwap/kwap/controllers/domain.py -kwap/kwap/controllers/error.py -kwap/kwap/controllers/__init__.py -kwap/kwap/controllers/root.py -kwap/kwap/controllers/secure.py -kwap/kwap/controllers/template.py -kwap/kwap/grids/domains.py -kwap/kwap/grids/__init__.py -kwap/kwap/__init__.py -kwap/kwap/lib/app_globals.py -kwap/kwap/lib/base.py -kwap/kwap/lib/helpers.py -kwap/kwap/lib/__init__.py -kwap/kwap/model/auth.py -kwap/kwap/model/__init__.py -kwap/kwap/templates/domain/__init__.py -kwap/kwap/templates/__init__.py -kwap/kwap/tests/functional/__init__.py -kwap/kwap/tests/functional/test_authentication.py -kwap/kwap/tests/functional/test_root.py -kwap/kwap/tests/__init__.py -kwap/kwap/tests/models/__init__.py -kwap/kwap/tests/models/test_auth.py -kwap/kwap/websetup/bootstrap.py -kwap/kwap/websetup/__init__.py -kwap/kwap/websetup/schema.py -kwap/setup.py -munich_demo.py -play/anon-imap/anon-imap.py -play/augeas-insert.py -play/base_64_decode.py -play/brepr_vs_unicode.py -play/cleanup_acls.py -play/cliconfmgmt.py -play/conf-add-file-to-service.py -play/conf-add-setting.py -play/conf-list-config-files.py -play/confmgmt/augeas.py -play/confmgmt/db.py -play/confmgmt/__init__.py -play/confmgmt/model.py -play/conf.py -play/conf-settings-from-file.py -play/conf-update-file.py -play/detect-object-type.py -play/dttz.py -play/effectiverights.py -play/flawed_zpush_testing_create_folders.py -play/fork.py -play/get_uid.py -play/imap_annotations_test.py -play/kolab-sap/kolab_smtp_access_policy.py -play/libkolabxml/contact.py -play/libkolabxml/event_imap.py -play/libkolabxml/event.py -play/libkolabxml/event_rfc822.py -play/libkolabxml/todo.py -play/load_test.py -play/migrate_lowercase_uid.py -play/migrate_mail_to_uid_prod.py -play/migrate_mail_to_uid.py -play/migrate_uid_to_mail_prod.py -play/migrate_uid_to_mail.py -play/noheaderini.py -play/not-an-itip-message.py -play/openssl/license.py -play/parallel_persistent_searches.py -play/parse_policy.py -play/persistent_search_kolab_23.py -play/persistent_search.py -play/pooling.py -play/purge_users_roundcube.py -play/push_contacts.py -play/pygpgme/sign.py -play/pygpgme/verify.py -play/regexps.py -play/rolequota/__init__.py -play/roundcube_database/identities.py -play/roundcube_database/__init__.py -play/roundcube_database.py -play/roundcube_database/users.py -play/split_message_file.py -play/sqlalchemy_schemadisplay.py -play/strip_many_headers.py -play/sync_client.py -play/sync_repl_kolab_23.py -play/test_augeas_load.py -play/test_cal_spread.py -play/test-entitlements.py -play/test_filter.py -play/test_folders.py -play/test-icalendar-attendee.py -play/test_imapd.py -play/test_imap.py -play/test-kolab-smtp-access-policy-load.py -play/test_kolabxml.py -play/test-login-as.py -play/test-namespace.py -play/test-output.py -play/test_page_control.py -play/test-parse_ldap_uri.py -play/test.py -play/test_sk.ks.c.py -play/test_socket_client.py -play/test_socket.py -play/test_submission.py -play/test_undelete.py -play/translit.py -play/unicode_test.py -play/unicode-to-ascii.py -play/wap/domain.info.py -play/wap/domains.capabilities.py -play/wap/domains.list.py -play/wap/form_value.generate_cn.py -play/wap/form_value.generate_displayname.py -play/wap/form_value.generate_mail.py -play/wap/form_value.generate_password.py -play/wap/form_value.generate_uid.py -play/wap/form_value.list_options-c.py -play/wap/form_value.select_options-ou.py -play/wap/form_value.select_options-preferredlanguage.py -play/wap/group.add.py -play/wap/group.info.py -play/wap/group.members_list.py -play/wap/groups.list.py -play/wap/group_types.list.py -play/wap/role.add.py -play/wap/role.capabilities.py -play/wap/role.delete.py -play/wap/role.find_by_attribute.py -play/wap/role.info.py -play/wap/roles.list.py -play/wap/scu.py -play/wap/system.capabilities.py -play/wap/system.select_domain.py -play/wap/user.add.py -play/wap/user.delete.py -play/wap/user.edit.py -play/wap/user.info.py -play/wap/users.list.py -play/wap/user_types.list.py -play/xmlevent.py -play/xmlformat.py -play/xmlfromical.py -play/zorbadb/test.py -pykolab/auth/ldap/fds/__init__.py -pykolab/auth/ldap/msds/__init__.py -pykolab/auth/ldap/openldap/__init__.py -pykolab/auth/ldap/rhds/__init__.py -pykolab/auth/ldap/sunds/__init__.py -pykolab/auth/sql/__init__.py -pykolab/cli/cmd_examine_message.py -pykolab/cli/cmd_list_contacts.py -pykolab/cli/cmd_list_events.py -pykolab/cli/cmd_rebalance_mailboxes.py -pykolab/cli/cmd_role_info.py -pykolab/cli/cmd_set_quota.py -pykolab/cli/cmd_summarize_quota_allocation.py -pykolab/confmgmt/augeas.py -pykolab/conf/parser.py +kolabd/.___init__.py +pykolab/auth/.___init__.py +pykolab/auth/ldap/._cache.py +pykolab/auth/ldap/.___init__.py +pykolab/._base.py +pykolab/cli/._cmd_create_mailbox.py +pykolab/cli/._cmd_list_mailbox_metadata.py +pykolab/cli/._cmd_list_quota.py +pykolab/cli/._cmd_set_language.py +pykolab/cli/._cmd_set_mailbox_acl.py +pykolab/cli/._cmd_set_mail.py +pykolab/conf/._defaults.py +pykolab/conf/.___init__.py +pykolab/._constants.py pykolab/constants.py -pykolab/ical/itip.py -pykolab/imap/dovecot.py -pykolab/plugins/roundcube/__init__.py -pykolab/wap_client.old/__init__.py -pykolab/xml/task.py -sievelib-0.5/setup.py -sievelib-0.5/sievelib/commands.py -sievelib-0.5/sievelib/digest_md5.py -sievelib-0.5/sievelib/factory.py -sievelib-0.5/sievelib/__init__.py -sievelib-0.5/sievelib/managesieve.py -sievelib-0.5/sievelib/parser.py -testaci.py -test-ask_menu.py -test-search.py -test-send-mail-kolab_smtp_access_policy.py -test-send-mail-kolab_smtp_access_policy-relay.py -tests/functional/test_kolabd/test_004_many_aliases.py -test-subscribe-address-to-ml.py -tests/unit/test-011-base64_encoded_contact.py -tests/unit/test-012-utf7.py -test-urllib.py -test-wallace-loadmsg.py -test-wallace-resource.py -test-wallace-send.py -wallace/future_module_bcc.py -wallace/future_module_conversations.py -wallace/future_module_correctsentdate.py -wallace/future_module_dlp.py -wallace/future_module_footer.py -wallace/future_module_freebusy.py -wallace/future_module_googletranslate.py -wallace/future_module_statistics.py -wap_raw.py +pykolab/._constants.py.in +pykolab/imap/._cyrus.py +pykolab/imap/.___init__.py +pykolab/.___init__.py +pykolab/itip/.___init__.py +pykolab/._logger.py +pykolab/plugins/dynamicquota/.___init__.py +pykolab/plugins/.___init__.py +pykolab/plugins/roundcubedb/.___init__.py +pykolab/setup/.___init__.py +pykolab/setup/._setup_freebusy.py +pykolab/setup/._setup_ldap.py +pykolab/setup/._setup_mta.py +pykolab/setup/._setup_roundcube.py +pykolab/._translate.py +pykolab/._translit.py +pykolab/._utils.py +pykolab/wap_client/.___init__.py +pykolab/xml/._attendee.py +pykolab/xml/._event.py +pykolab/xml/.___init__.py +pykolab/xml/._utils.py +tests/functional/._purge_users.py +tests/functional/._resource_func.py +tests/functional/._synchronize.py +tests/functional/test_auth/.___init__.py +tests/functional/test_auth/._test_001_ldap.py +tests/functional/test_auth/._test_002_sql.py +tests/functional/test_auth/._test_003_pam.py +tests/functional/test_auth/._test_004_saslauthd.py +tests/functional/test_kolabd/._test_001_user_sync.py +tests/functional/test_wallace/._test_001_user_add.py +tests/functional/test_wallace/._test_002_footer.py +tests/functional/test_wallace/._test_005_resource_add.py +tests/functional/test_wallace/._test_005_resource_invitation.py +tests/functional/test_wallace/._test_006_resource_performance.py +tests/functional/test_wallace/._test_007_invitationpolicy.py +tests/functional/test_wap_client/._test_002_user_add.py +tests/functional/._user_add.py +tests/unit/._test-000-imports.py +tests/unit/._test-002-attendee.py +tests/unit/._test-003-event.py +tests/unit/._test-006-ldap_psearch.py +tests/unit/._test-007-ldap_syncrepl.py +tests/unit/._test-010-transliterate.py +tests/unit/._test-011-itip.py +tests/unit/._test-011-wallace_resources.py +tests/unit/._test-012-wallace_invitationpolicy.py +tests/unit/._test-014-conf-and-raw.py +tests/unit/._test-015-translate.py +wallace/.___init__.py +wallace/._module_gpgencrypt.py +wallace/._module_invitationpolicy.py +wallace/._module_resources.py +wallace/._modules.py @@ -3,331 +3,362 @@ # This file is distributed under the same license as the PACKAGE package. # # Translators: -# Christoph Wickert <cwickert@fedoraproject.org>, 2011. -# <grote@kolabsys.com>, 2012. +# Christoph Wickert <christoph.wickert@gmail.com>, 2011 +# Grote <grote@kolabsys.com>, 2012 +# balin <johannes_graumann@web.de>, 2012 +# Jo <jo@caj-augsburg.de>, 2012 +# bitnukl <robert@proemper.net>, 2014 +# Thomas Brüderli <roundcube@gmail.com>, 2014 +# Till Savekoul <till@koul.de>, 2012 msgid "" msgstr "" "Project-Id-Version: Kolab Groupware Solution\n" -"Report-Msgid-Bugs-To: https://isues.kolab.org/\n" -"POT-Creation-Date: 2012-08-14 12:22+0100\n" -"PO-Revision-Date: 2012-08-14 11:13+0000\n" -"Last-Translator: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2014-07-10 07:21-0400\n" +"PO-Revision-Date: 2014-07-22 13:01+0000\n" +"Last-Translator: Thomas Brüderli <roundcube@gmail.com>\n" "Language-Team: German (http://www.transifex.com/projects/p/kolab/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: de\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: ../bin/kolab_smtp_access_policy.py:206 +#: ../bin/kolab_smtp_access_policy.py:209 #, python-format msgid "Adding policy request to instance %s" msgstr "Füge Richtlinien-Anfrage zu Instanz %s hinzu" -#: ../bin/kolab_smtp_access_policy.py:446 +#: ../bin/kolab_smtp_access_policy.py:479 msgid "Unauthorized access not allowed" msgstr "Unberechtigter Zugriff nicht erlaubt" -#: ../bin/kolab_smtp_access_policy.py:467 -#: ../bin/kolab_smtp_access_policy.py:657 +#: ../bin/kolab_smtp_access_policy.py:508 +#: ../bin/kolab_smtp_access_policy.py:689 msgid "Could not find recipient" msgstr "Konnte den Empfänger nicht finden" -#: ../bin/kolab_smtp_access_policy.py:486 -#: ../bin/kolab_smtp_access_policy.py:586 +#: ../bin/kolab_smtp_access_policy.py:527 #, python-format -msgid "Could not find envelope sender user %s" -msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden" +msgid "Could not find envelope sender user %s (511)" +msgstr "" -#: ../bin/kolab_smtp_access_policy.py:529 +#: ../bin/kolab_smtp_access_policy.py:570 #, python-format msgid "Obtained authenticated user details for %r: %r" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:608 +#: ../bin/kolab_smtp_access_policy.py:627 +#, python-format +msgid "Could not find envelope sender user %s" +msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden" + +#: ../bin/kolab_smtp_access_policy.py:649 #, python-format msgid "%s is unauthorized to send on behalf of %s" msgstr "Benutzer %s ist nicht berechtigt als Benutzer %s zu senden" -#: ../bin/kolab_smtp_access_policy.py:618 +#: ../bin/kolab_smtp_access_policy.py:659 #, python-format msgid "" "User %s attempted to use envelope sender address %s without authorization" -msgstr "" +msgstr "Benutzer %s versuchte die Absendeadresse %s ohne Berechtigung zu verwenden" -#: ../bin/kolab_smtp_access_policy.py:681 -#: ../bin/kolab_smtp_access_policy.py:692 +#: ../bin/kolab_smtp_access_policy.py:713 +#: ../bin/kolab_smtp_access_policy.py:724 #, python-format msgid "Found user %s to be a delegate user of %s" msgstr "Benutzer %s ist ein delegierter Benutzer von %s" -#: ../bin/kolab_smtp_access_policy.py:716 +#: ../bin/kolab_smtp_access_policy.py:748 #, python-format msgid "" "Verifying authenticated sender '%(sender)s' with sasl_username " "'%(sasl_username)s' for recipient '%(recipient)s'" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:719 +#: ../bin/kolab_smtp_access_policy.py:751 #, python-format msgid "" "Verifying unauthenticated sender '%(sender)s' for recipient '%(recipient)s'" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:735 +#: ../bin/kolab_smtp_access_policy.py:767 #, python-format msgid "Reproducing verify_recipient(%s, %s) from cache" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:753 +#: ../bin/kolab_smtp_access_policy.py:804 #, python-format msgid "Using authentication domain %s instead of %s" msgstr "Benutze Authentisierungsdomain %s anstelle von %s" -#: ../bin/kolab_smtp_access_policy.py:763 +#: ../bin/kolab_smtp_access_policy.py:814 #, python-format msgid "Domain %s is a primary domain" msgstr "Die Domain %s ist die primäre Domain" -#: ../bin/kolab_smtp_access_policy.py:771 +#: ../bin/kolab_smtp_access_policy.py:822 #, python-format msgid "" "Checking the recipient for domain %s that is not ours. This is probably a " "configuration error." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:786 +#: ../bin/kolab_smtp_access_policy.py:837 msgid "" "This recipient address is related to multiple object entries and the SMTP " "Access Policy can therefore not restrict message flow" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:793 +#: ../bin/kolab_smtp_access_policy.py:854 #, python-format msgid "" "Recipient address %r not found. Allowing since the MTA was configured to " "accept the recipient." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:820 +#: ../bin/kolab_smtp_access_policy.py:890 msgid "Invalid recipient" msgstr "Ungültiger Empfänger" -#: ../bin/kolab_smtp_access_policy.py:831 +#: ../bin/kolab_smtp_access_policy.py:901 msgid "Could not find this user, accepting" msgstr "Konnte keine Einschränkung für diesen Benutzer finden, akzeptiere Nachricht" -#: ../bin/kolab_smtp_access_policy.py:894 -#: ../bin/kolab_smtp_access_policy.py:945 +#: ../bin/kolab_smtp_access_policy.py:974 +#: ../bin/kolab_smtp_access_policy.py:1050 #, python-format msgid "Sender %s is not allowed to send to recipient %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:933 +#: ../bin/kolab_smtp_access_policy.py:1038 #, python-format msgid "Reproducing verify_sender(%r) from cache" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:960 +#: ../bin/kolab_smtp_access_policy.py:1055 +msgid "Unverifiable sender." +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1060 +msgid "Sender is not using an alias" +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1068 msgid "Sender uses unauthorized envelope sender address" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:977 +#: ../bin/kolab_smtp_access_policy.py:1085 msgid "Could not verify sender" msgstr "Konnte den Absender nicht verifizieren" -#: ../bin/kolab_smtp_access_policy.py:984 +#: ../bin/kolab_smtp_access_policy.py:1092 msgid "" "Verifying whether sender is allowed to send to recipient using sender policy" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:997 +#: ../bin/kolab_smtp_access_policy.py:1105 #, python-format msgid "Result is %r" msgstr "Das Ergebnis ist %r" -#: ../bin/kolab_smtp_access_policy.py:1002 +#: ../bin/kolab_smtp_access_policy.py:1110 msgid "No recipient policy restrictions exist for this sender" msgstr "Es existiert keine Empfängerrichtlinie für diesen Absender" -#: ../bin/kolab_smtp_access_policy.py:1011 +#: ../bin/kolab_smtp_access_policy.py:1119 msgid "Found a recipient policy to apply for this sender." msgstr "Empfänger-Richtlinie für diesen Benutzer gefunden" -#: ../bin/kolab_smtp_access_policy.py:1026 +#: ../bin/kolab_smtp_access_policy.py:1134 #, python-format msgid "Sender %s not allowed to send to recipient %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1056 +#: ../bin/kolab_smtp_access_policy.py:1155 msgid "Cleaning up the cache" msgstr "Aufräumen des Cache" -#: ../bin/kolab_smtp_access_policy.py:1093 +#: ../bin/kolab_smtp_access_policy.py:1177 +msgid "" +"The 'uri' setting in the kolab_smtp_access_policy section is soon going to " +"be deprecated in favor of 'cache_uri'" +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1193 #, python-format msgid "Operational Error in caching: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1144 +#: ../bin/kolab_smtp_access_policy.py:1245 #, python-format msgid "Caching the policy result with timestamp %d" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1221 +#: ../bin/kolab_smtp_access_policy.py:1319 #, python-format msgid "Returning action DEFER_IF_PERMIT: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1226 +#: ../bin/kolab_smtp_access_policy.py:1324 #, python-format msgid "Returning action DUNNO: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1231 +#: ../bin/kolab_smtp_access_policy.py:1329 #, python-format msgid "Returning action HOLD: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1236 +#: ../bin/kolab_smtp_access_policy.py:1334 #, python-format msgid "Returning action PERMIT: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1241 +#: ../bin/kolab_smtp_access_policy.py:1459 #, python-format msgid "Returning action REJECT: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1287 +#: ../bin/kolab_smtp_access_policy.py:1505 msgid "Starting to loop for new request" msgstr "Starte Schleife für neue Anfrage" -#: ../bin/kolab_smtp_access_policy.py:1294 +#: ../bin/kolab_smtp_access_policy.py:1512 msgid "Timeout for policy request reading exceeded" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1300 +#: ../bin/kolab_smtp_access_policy.py:1518 msgid "End of current request" msgstr "Ende der aktuellen Anfrage" -#: ../bin/kolab_smtp_access_policy.py:1304 +#: ../bin/kolab_smtp_access_policy.py:1522 #, python-format msgid "Getting line: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1308 +#: ../bin/kolab_smtp_access_policy.py:1526 msgid "Returning request" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1334 +#: ../bin/kolab_smtp_access_policy.py:1555 msgid "Access Policy Options" msgstr "Zugriffsrichtlinien-Einstellungen" -#: ../bin/kolab_smtp_access_policy.py:1341 +#: ../bin/kolab_smtp_access_policy.py:1562 msgid "SMTP Policy request timeout." msgstr "Zeitüberschreitung der SMTP Richtlinien-Anfrage" -#: ../bin/kolab_smtp_access_policy.py:1347 +#: ../bin/kolab_smtp_access_policy.py:1568 msgid "Verify the recipient access policy." msgstr "Verifiziere die Empfänger-Zugriffs-Richtlinie." -#: ../bin/kolab_smtp_access_policy.py:1353 +#: ../bin/kolab_smtp_access_policy.py:1574 msgid "Verify the sender access policy." msgstr "Verifiziere die Sender-Zugriffs-Richtlinie." -#: ../bin/kolab_smtp_access_policy.py:1359 +#: ../bin/kolab_smtp_access_policy.py:1580 msgid "Allow unauthenticated senders." msgstr "Erlaube nicht authentisierte Sender." -#: ../bin/kolab_smtp_access_policy.py:1373 +#: ../bin/kolab_smtp_access_policy.py:1594 #, python-format msgid "Got request instance %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1382 +#: ../bin/kolab_smtp_access_policy.py:1603 #, python-format msgid "Request instance %s is in state %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1390 +#: ../bin/kolab_smtp_access_policy.py:1611 #, python-format msgid "Request instance %s is not yet in DATA state" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1402 +#: ../bin/kolab_smtp_access_policy.py:1623 #, python-format msgid "Request instance %s reached DATA state" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1420 +#: ../bin/kolab_smtp_access_policy.py:1643 +#, python-format +msgid "Unhandled exception caught: %r" +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1647 msgid "Sender access denied" msgstr "Sender Zugriff verweigert" -#: ../bin/kolab_smtp_access_policy.py:1422 +#: ../bin/kolab_smtp_access_policy.py:1649 msgid "Recipient access denied" msgstr "Empfänger Zugriff verweigert" -#: ../bin/kolab_smtp_access_policy.py:1424 +#: ../bin/kolab_smtp_access_policy.py:1651 msgid "No objections" msgstr "Keine Einwände" -#: ../conf.py:37 ../kolab.py:34 ../saslauthd.py:33 +#: ../conf.py:37 ../kolab-cli.py:34 ../saslauthd.py:33 msgid "Cannot load pykolab/logger.py:" -msgstr "" +msgstr "Laden von pykolab/logger.py nicht möglich:" -#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:48 -#: ../wallace/__init__.py:65 +#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:51 +#: ../wallace/__init__.py:85 msgid "Daemon Options" msgstr "Daemon Optionen" -#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:55 -#: ../wallace/__init__.py:72 +#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:58 +#: ../wallace/__init__.py:92 msgid "Fork to the background." msgstr "In den Hintergrund abtauchen" -#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:64 -#: ../wallace/__init__.py:98 +#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:67 +#: ../wallace/__init__.py:118 msgid "Path to the PID file to use." msgstr "Pfad zur PID-Datei" -#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:73 -#: ../wallace/__init__.py:115 +#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76 +#: ../wallace/__init__.py:135 msgid "Run as user USERNAME" -msgstr "" +msgstr "Als Benutzer USERNAME ausführen" -#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:83 -#: ../wallace/__init__.py:89 +#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86 +#: ../wallace/__init__.py:109 msgid "Run as group GROUPNAME" -msgstr "" +msgstr "Als Gruppe GROUPNAME ausführen" -#: ../kolabd/__init__.py:122 ../pykolab/utils.py:151 -#: ../wallace/__init__.py:288 +#: ../kolabd/__init__.py:122 ../pykolab/logger.py:139 ../pykolab/utils.py:234 +#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:329 #, python-format msgid "Group %s does not exist" -msgstr "" +msgstr "Gruppe %s exisitert nicht" -#: ../kolabd/__init__.py:131 ../wallace/__init__.py:297 +#: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301 +#: ../wallace/__init__.py:338 #, python-format msgid "Switching real and effective group id to %d" msgstr "" -#: ../kolabd/__init__.py:153 ../pykolab/utils.py:175 -#: ../wallace/__init__.py:319 +#: ../kolabd/__init__.py:153 ../pykolab/logger.py:159 ../pykolab/utils.py:258 +#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:360 #, python-format msgid "User %s does not exist" msgstr "Benutzer %s existiert nicht" -#: ../kolabd/__init__.py:163 ../wallace/__init__.py:329 +#: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333 +#: ../wallace/__init__.py:370 #, python-format msgid "Switching real and effective user id to %d" msgstr "" -#: ../kolabd/__init__.py:172 ../wallace/__init__.py:338 +#: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342 +#: ../wallace/__init__.py:379 msgid "Could not change real and effective uid and/or gid" msgstr "" -#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:122 -#: ../wallace/__init__.py:358 +#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133 +#: ../wallace/__init__.py:399 msgid "Interrupted by user" msgstr "Vom Benutzer unterbrochen" @@ -335,328 +366,492 @@ msgstr "Vom Benutzer unterbrochen" msgid "Traceback occurred, please report a " msgstr "" -#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:130 -#: ../wallace/__init__.py:367 +#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141 +#: ../wallace/__init__.py:408 #, python-format msgid "Type Error: %s" msgstr "Typ-Fehler: %s" -#: ../kolabd/__init__.py:223 ../pykolab/auth/ldap/__init__.py:1591 +#: ../kolabd/__init__.py:230 +msgid "Could not connect to LDAP, is it running?" +msgstr "" + +#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137 #: ../pykolab/cli/cmd_sync.py:36 msgid "Listing domains..." msgstr "Liste Domains auf..." -#: ../kolabd/__init__.py:256 +#: ../kolabd/__init__.py:244 +msgid "No domains. Not syncing" +msgstr "" + +#: ../kolabd/__init__.py:275 #, python-format msgid "added domains: %r, removed domains: %r" msgstr "" +#: ../kolabd/process.py:33 +#, python-format +msgid "Process created for domain %s" +msgstr "" + +#: ../kolabd/process.py:42 +#, python-format +msgid "Synchronizing for domain %s" +msgstr "" + +#: ../kolabd/process.py:59 +#, python-format +msgid "" +"Error in process %r, terminating:\n" +"\t%r" +msgstr "" + #: ../kolabd.py:31 ../setup-kolab.py:36 ../wallace.py:31 msgid "Cannot load pykolab/constants.py:" msgstr "Konnte nicht pykolab/constants.py laden:" -#: ../pykolab/auth/__init__.py:94 +#: ../pykolab/auth/__init__.py:89 #, python-format msgid "Called for domain %r" msgstr "" -#: ../pykolab/auth/__init__.py:107 ../pykolab/auth/__init__.py:116 +#: ../pykolab/auth/__init__.py:106 ../pykolab/auth/__init__.py:115 #, python-format msgid "Using section %s and domain %s" msgstr "" -#: ../pykolab/auth/__init__.py:121 +#: ../pykolab/auth/__init__.py:120 #, python-format msgid "Connecting to Authentication backend for domain %s" msgstr "" -#: ../pykolab/auth/__init__.py:132 +#: ../pykolab/auth/__init__.py:131 #, python-format msgid "Section %s has no option 'auth_mechanism'" msgstr "" -#: ../pykolab/auth/__init__.py:139 +#: ../pykolab/auth/__init__.py:138 #, python-format msgid "Section %s has auth_mechanism: %r" msgstr "" -#: ../pykolab/auth/__init__.py:148 ../pykolab/auth/__init__.py:157 +#: ../pykolab/auth/__init__.py:147 ../pykolab/auth/__init__.py:156 msgid "Starting LDAP..." msgstr "Starte LDAP..." -#: ../pykolab/auth/ldap/cache.py:109 +#: ../pykolab/auth/ldap/cache.py:126 #, python-format msgid "Inserting cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/cache.py:122 +#: ../pykolab/auth/ldap/cache.py:147 #, python-format msgid "Updating timestamp for cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/cache.py:129 +#: ../pykolab/auth/ldap/cache.py:155 #, python-format msgid "Updating result_attribute for cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:51 +#: ../pykolab/auth/ldap/__init__.py:52 msgid "Python LDAP library does not support persistent search" msgstr "Die Python LDAP Bibliothek unterstützt keine persistente Suche" -#: ../pykolab/auth/ldap/__init__.py:142 +#: ../pykolab/auth/ldap/__init__.py:143 #, python-format msgid "Attempting to authenticate user %s in realm %s" msgstr "Versuche Benutzer %s in Bereich %s zu authentisieren" -#: ../pykolab/auth/ldap/__init__.py:184 +#: ../pykolab/auth/ldap/__init__.py:175 ../pykolab/auth/ldap/__init__.py:226 +#, python-format +msgid "Authentication cache failed: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:216 ../pykolab/auth/ldap/__init__.py:240 #, python-format msgid "Binding with user_dn %s and password %s" msgstr "Binde mit user_dn %s und Passwort %s" -#: ../pykolab/auth/ldap/__init__.py:194 +#: ../pykolab/auth/ldap/__init__.py:231 ../pykolab/auth/ldap/__init__.py:263 #, python-format msgid "Failed to authenticate as user %s" +msgstr "Autorisation als Benutzer %s gescheitert" + +#: ../pykolab/auth/ldap/__init__.py:249 +#, python-format +msgid "Error occured, there is no such object: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:254 +msgid "Authentication cache failed to clear entry" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:260 +#, python-format +msgid "Exception occured: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:211 +#: ../pykolab/auth/ldap/__init__.py:280 msgid "Connecting to LDAP..." msgstr "Verbinde zum LDAP..." -#: ../pykolab/auth/ldap/__init__.py:215 +#: ../pykolab/auth/ldap/__init__.py:284 #, python-format msgid "Attempting to use LDAP URI %s" msgstr "Versuche LDAP URI %s zu benutzen" -#: ../pykolab/auth/ldap/__init__.py:357 +#: ../pykolab/auth/ldap/__init__.py:371 +#, python-format +msgid "Entry ID: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:373 +#, python-format +msgid "Entry DN: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:376 +#, python-format +msgid "" +"ldap search: (%r, %r, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:453 #, python-format msgid "Finding recipient with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:425 +#: ../pykolab/auth/ldap/__init__.py:529 #, python-format msgid "Finding resource with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:448 +#: ../pykolab/auth/ldap/__init__.py:560 #, python-format msgid "Using timestamp %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:480 +#: ../pykolab/auth/ldap/__init__.py:595 +msgid "Applying recipient policy disabled through configuration" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:600 #, python-format msgid "Applying recipient policy to %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:497 +#: ../pykolab/auth/ldap/__init__.py:617 #, python-format msgid "Using mail attributes: %r, with primary %r and " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:508 +#: ../pykolab/auth/ldap/__init__.py:628 #, python-format msgid "key %r not in entry" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:510 +#: ../pykolab/auth/ldap/__init__.py:630 #, python-format msgid "key %r is the prim. mail attr." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:512 +#: ../pykolab/auth/ldap/__init__.py:632 msgid "prim. mail pol. is not empty" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:515 +#: ../pykolab/auth/ldap/__init__.py:635 #, python-format msgid "key %r is the sec. mail attr." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:517 +#: ../pykolab/auth/ldap/__init__.py:637 msgid "sec. mail pol. is not empty" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:520 ../pykolab/auth/ldap/__init__.py:533 +#: ../pykolab/auth/ldap/__init__.py:641 ../pykolab/auth/ldap/__init__.py:655 #, python-format msgid "Attributes %r are not yet available for entry %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:574 +#: ../pykolab/auth/ldap/__init__.py:694 #, python-format msgid "No results for mail address %s found" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:585 +#: ../pykolab/auth/ldap/__init__.py:705 #, python-format msgid "1 result for address %s found, verifying" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:595 +#: ../pykolab/auth/ldap/__init__.py:715 #, python-format msgid "Too bad, primary email address %s " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:606 ../pykolab/auth/ldap/__init__.py:695 +#: ../pykolab/auth/ldap/__init__.py:726 ../pykolab/auth/ldap/__init__.py:815 msgid "Address assigned to us" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:661 +#: ../pykolab/auth/ldap/__init__.py:781 #, python-format msgid "No results for address %s found" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:672 +#: ../pykolab/auth/ldap/__init__.py:792 #, python-format msgid "1 result for address %s found, " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:683 +#: ../pykolab/auth/ldap/__init__.py:803 msgid "Too bad, secondary email " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:710 +#: ../pykolab/auth/ldap/__init__.py:830 msgid "Recipient policy composed the following set of secondary " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:716 +#: ../pykolab/auth/ldap/__init__.py:841 #, python-format msgid "Secondary mail addresses that we want is not None: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:727 +#: ../pykolab/auth/ldap/__init__.py:852 msgid "Avoiding the duplication of the primary mail " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:738 +#: ../pykolab/auth/ldap/__init__.py:863 #, python-format msgid "Entry is getting secondary mail addresses: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:746 +#: ../pykolab/auth/ldap/__init__.py:871 msgid "Entry did not have any secondary mail " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:772 +#: ../pykolab/auth/ldap/__init__.py:888 ../pykolab/auth/ldap/__init__.py:894 +#, python-format +msgid "secondary_mail_addresses: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:889 ../pykolab/auth/ldap/__init__.py:895 +#, python-format +msgid "entry[%s]: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:906 #, python-format msgid "Entry modifications list: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:792 +#: ../pykolab/auth/ldap/__init__.py:934 #, python-format msgid "Setting entry attribute %r to %r for %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:828 +#: ../pykolab/auth/ldap/__init__.py:970 #, python-format -msgid "Could not update dn %r" +msgid "" +"Could not update dn %r:\n" +"%r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:841 +#: ../pykolab/auth/ldap/__init__.py:983 #, python-format msgid "Using filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:879 +#: ../pykolab/auth/ldap/__init__.py:998 +#, python-format +msgid "Synchronization is searching against base DN: %s" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1044 #, python-format msgid "About to consider the user quota for %r (used: %r, " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:945 +#: ../pykolab/auth/ldap/__init__.py:1115 msgid "Invalid DN, username and/or password." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1229 +#: ../pykolab/auth/ldap/__init__.py:1236 ../pykolab/auth/ldap/__init__.py:1249 +#: ../pykolab/auth/ldap/__init__.py:1614 ../pykolab/auth/ldap/__init__.py:1627 +#, python-format +msgid "Found a subject %r with access %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1356 +#, python-format +msgid "Entry %s attribute value: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1364 #, python-format -msgid "Current changelog entry %s with %s" +msgid "imap.user_mailbox_server(%r) result: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1242 +#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853 #, python-format msgid "Result from recipient policy: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1385 +#: ../pykolab/auth/ldap/__init__.py:1908 #, python-format msgid "Kolab user %s does not have a result attribute %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1527 +#: ../pykolab/auth/ldap/__init__.py:2067 #, python-format msgid "Finding domain root dn for domain %s" -msgstr "" +msgstr "Suche root dn für die Domain %s" -#: ../pykolab/auth/ldap/__init__.py:1615 +#: ../pykolab/auth/ldap/__init__.py:2164 msgid "Authentication database DOWN" msgstr "Authentisierungsdatenbank UNTEN" -#: ../pykolab/auth/ldap/__init__.py:1699 ../pykolab/auth/ldap/__init__.py:1734 +#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296 #, python-format msgid "Entry type: %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1822 +#: ../pykolab/auth/ldap/__init__.py:2321 +#, python-format +msgid "Done with _synchronize_callback() for entry %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:2393 msgid "LDAP Search Result Data Entry:" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1838 +#: ../pykolab/auth/ldap/__init__.py:2409 msgid "Entry Change Notification attributes:" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1843 +#: ../pykolab/auth/ldap/__init__.py:2414 #, python-format msgid "Change Type: %r (%r)" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1851 +#: ../pykolab/auth/ldap/__init__.py:2422 #, python-format msgid "Previous DN: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1906 +#: ../pykolab/auth/ldap/__init__.py:2477 #, python-format msgid "Object %s searched no longer exists" msgstr "Das gesuchte Objekt %s existiert nicht mehr" -#: ../pykolab/auth/ldap/__init__.py:1916 +#: ../pykolab/auth/ldap/__init__.py:2487 #, python-format msgid "%d results..." msgstr "%d Ergebnisse..." -#: ../pykolab/auth/ldap/__init__.py:2014 +#: ../pykolab/auth/ldap/__init__.py:2590 #, python-format msgid "Searching with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2058 +#: ../pykolab/auth/ldap/__init__.py:2642 #, python-format msgid "Checking for support for %s on %s" msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:36 ../pykolab/cli/cmd_create_mailbox.py:36 +#: ../pykolab/auth/ldap/__init__.py:2661 +#, python-format +msgid "Found support for %s" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:2706 +#, python-format +msgid "An error occured using %s: %r" +msgstr "" + +#: ../pykolab/auth/ldap/syncrepl.py:46 +msgid "The name of the persistent, unique attribute " +msgstr "" + +#: ../pykolab/cli/cmd_acl_cleanup.py:34 +msgid "Clean up ACLs that use identifiers that no longer exist" +msgstr "" + +#: ../pykolab/cli/cmd_acl_cleanup.py:56 +#, python-format +msgid "Deleting ACL %s for subject %s on folder %s" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:42 +msgid "Specify the (new) alias address" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:45 +msgid "Specify the existing recipient address" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:66 ../pykolab/cli/cmd_add_alias.py:70 +#, python-format +msgid "Domain %r is not a local domain" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:75 +msgid "Primary and secondary domain do not have the same parent domain" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:81 +#, python-format +msgid "No such recipient %r" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:87 +#, python-format +msgid "Recipient for alias %r already exists" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:97 +msgid "Environment is not configured for " +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:105 +#, python-format +msgid "Recipient %r is not the primary recipient for address %r" +msgstr "" + +#: ../pykolab/cli/cmd_add_domain.py:36 +#: ../pykolab/cli/cmd_count_domain_mailboxes.py:38 +#: ../pykolab/cli/cmd_create_mailbox.py:36 #: ../pykolab/cli/cmd_export_mailbox.py:33 -#: ../pykolab/cli/cmd_list_mailboxes.py:39 -#: ../pykolab/cli/cmd_list_user_subscriptions.py:35 +#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:38 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:36 +#: ../pykolab/cli/cmd_list_mailboxes.py:40 +#: ../pykolab/cli/cmd_list_mailbox_metadata.py:37 +#: ../pykolab/cli/cmd_list_messages.py:37 ../pykolab/cli/cmd_list_quota.py:36 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:36 +#: ../pykolab/cli/cmd_server_info.py:34 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:38 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:39 +#: ../pykolab/cli/cmd_undelete_mailbox.py:34 msgid "CLI Options" msgstr "Kommandozeilen-Parameter" #: ../pykolab/cli/cmd_add_domain.py:42 -msgid "Add domain as alias for DOMAIN" +msgid "Add alias domain." msgstr "" #: ../pykolab/cli/cmd_add_domain.py:47 -msgid "Add a new domain or domain alias." -msgstr "Füge eine neue Domain oder einen neuen Domain Alias hinzu." - -#: ../pykolab/cli/cmd_add_domain.py:55 -msgid "Could not find credentials with sufficient permissions" +msgid "Add a new domain." msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:80 ../pykolab/wap_client/__init__.py:113 -msgid "Invalid parent domain" +#: ../pykolab/cli/cmd_add_domain.py:55 ../pykolab/cli/cmd_delete_domain.py:44 +#: ../pykolab/cli/cmd_find_domain.py:44 +msgid "Could not find credentials with sufficient permissions" msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:86 +#: ../pykolab/cli/cmd_add_domain.py:67 ../pykolab/cli/cmd_delete_domain.py:56 +#: ../pykolab/cli/cmd_find_domain.py:56 msgid "Domain name" -msgstr "" +msgstr "Name der Domain" #: ../pykolab/cli/cmd_add_user_subscription.py:37 msgid "Subscribe a user to a folder." @@ -670,7 +865,7 @@ msgid "Folder pattern" msgstr "" #: ../pykolab/cli/cmd_add_user_subscription.py:50 -#: ../pykolab/cli/cmd_list_user_subscriptions.py:56 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:63 #: ../pykolab/cli/cmd_remove_user_subscription.py:50 msgid "User ID" msgstr "Benutzer ID" @@ -682,30 +877,39 @@ msgid "Cannot subscribe user to folder %r:" msgstr "" #: ../pykolab/cli/cmd_add_user_subscription.py:73 +#: ../pykolab/cli/cmd_delete_message.py:61 +#: ../pykolab/cli/cmd_list_messages.py:67 #: ../pykolab/cli/cmd_remove_user_subscription.py:73 msgid "No such folder" -msgstr "" +msgstr "Dieser Ordner ist nicht vorhanden" -#: ../pykolab/cli/cmd_add_user_subscription.py:86 -#, python-format -msgid "Successfully subscribed user %s to the following folders:" -msgstr "" - -#: ../pykolab/cli/cmd_add_user_subscription.py:92 -#, python-format -msgid "User %s not subscribed to any folders." +#: ../pykolab/cli/cmd_count_domain_mailboxes.py:44 +#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:50 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:48 +#: ../pykolab/cli/cmd_list_mailboxes.py:52 ../pykolab/cli/cmd_list_quota.py:42 +#: ../pykolab/cli/cmd_server_info.py:40 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:57 +msgid "List mailboxes on server SERVER only." msgstr "" -#: ../pykolab/cli/cmd_create_mailbox.py:41 +#: ../pykolab/cli/cmd_create_mailbox.py:42 msgid "Set metadata for folder to ANNOTATION=VALUE" msgstr "" #: ../pykolab/cli/cmd_create_mailbox.py:50 -msgid "Invalid argument" +msgid "Create folder on PARTITION." msgstr "" -#: ../pykolab/cli/cmd_create_mailbox.py:58 +#: ../pykolab/cli/cmd_create_mailbox.py:60 +msgid "Invalid argument" +msgstr "Ungültiges Argument" + +#: ../pykolab/cli/cmd_create_mailbox.py:68 msgid "Invalid argument for metadata" +msgstr "Ungültiges Argument für die Metadaten" + +#: ../pykolab/cli/cmd_delete_domain.py:36 +msgid "Delete a domain." msgstr "" #: ../pykolab/cli/cmd_delete_mailbox_acl.py:45 @@ -716,26 +920,48 @@ msgid "ACI Subject" msgstr "" #: ../pykolab/cli/cmd_delete_mailbox_acl.py:48 -#: ../pykolab/cli/cmd_list_mailbox_acls.py:41 -#: ../pykolab/cli/cmd_list_mailbox_metadata.py:41 +#: ../pykolab/cli/cmd_list_mailbox_acls.py:43 +#: ../pykolab/cli/cmd_list_mailbox_metadata.py:54 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:54 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:66 +#: ../pykolab/cli/cmd_set_quota.py:46 ../tests/unit/test-015-translate.py:12 +#: ../tests/unit/test-015-translate.py:16 +#: ../tests/unit/test-015-translate.py:18 +#: ../tests/unit/test-015-translate.py:20 msgid "Folder name" -msgstr "" +msgstr "Ordnername" #: ../pykolab/cli/cmd_delete_mailbox_acl.py:60 -#: ../pykolab/cli/cmd_list_mailbox_acls.py:52 -#: ../pykolab/cli/cmd_list_mailbox_metadata.py:52 +#: ../pykolab/cli/cmd_list_mailbox_acls.py:54 +#: ../pykolab/cli/cmd_list_mailbox_metadata.py:80 #: ../pykolab/cli/cmd_set_mailbox_acl.py:67 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:94 +#: ../pykolab/cli/cmd_set_quota.py:58 #, python-format msgid "No such folder %r" -msgstr "" +msgstr "Kein Ordner %r verfügbar" #: ../pykolab/cli/cmd_delete_mailbox.py:46 msgid "No mailbox specified" msgstr "Keine Mailbox angegeben" +#: ../pykolab/cli/cmd_delete_mailbox.py:56 +msgid "No such folder(s)" +msgstr "" + +#: ../pykolab/cli/cmd_delete_message.py:36 +msgid "Delete a message from a folder" +msgstr "" + +#: ../pykolab/cli/cmd_delete_message.py:49 +msgid "Specify a UID" +msgstr "" + +#: ../pykolab/cli/cmd_delete_message.py:52 +#: ../pykolab/cli/cmd_list_messages.py:58 +msgid "Specify a folder" +msgstr "" + #: ../pykolab/cli/cmd_export_mailbox.py:38 msgid "All folders this user has access to" msgstr "" @@ -743,40 +969,77 @@ msgstr "" #: ../pykolab/cli/cmd_export_mailbox.py:108 #, python-format msgid "%s is not a directory" -msgstr "" +msgstr "%s ist kein Verzeichnis" #: ../pykolab/cli/cmd_export_mailbox.py:118 #, python-format msgid "ZIP file at %s.zip" -msgstr "" +msgstr "ZIP-Datei unter %s.zip" #: ../pykolab/cli/cmd_export_mailbox.py:120 #, python-format msgid "No directories found for user %s" msgstr "" -#: ../pykolab/cli/cmd_list_mailboxes.py:44 +#: ../pykolab/cli/cmd_find_domain.py:36 +msgid "Find a domain." +msgstr "" + +#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:43 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:41 +#: ../pykolab/cli/cmd_list_mailboxes.py:45 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:41 msgid "Display raw IMAP UTF-7 folder names" msgstr "" -#: ../pykolab/cli/cmd_list_mailboxes.py:75 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:58 +msgid "Domain" +msgstr "Domäne" + +#: ../pykolab/cli/cmd_list_mailboxes.py:87 #, python-format msgid "Appending folder search for %r" msgstr "" -#: ../pykolab/cli/cmd_list_user_subscriptions.py:40 +#: ../pykolab/cli/cmd_list_mailbox_metadata.py:44 +msgid "List annotations as user USER" +msgstr "" + +#: ../pykolab/cli/cmd_list_messages.py:43 +msgid "Include messages flagged as \\Deleted" +msgstr "" + +#: ../pykolab/cli/cmd_list_messages.py:47 +msgid "List messages in a folder" +msgstr "" + +#: ../pykolab/cli/cmd_list_quota.py:73 ../pykolab/cli/cmd_list_quota.py:89 +#, python-format +msgid "The quota for folder %s is set to literally allow 0KB of storage." +msgstr "" + +#: ../pykolab/cli/cmd_list_user_subscriptions.py:47 msgid "List unsubscribed folders" msgstr "Liste nicht abonnierte Ordner" -#: ../pykolab/cli/cmd_list_user_subscriptions.py:43 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:50 msgid "List the folders a user is subscribed to." msgstr "Liste die Ordner, die ein Benutzer abonniert hat." -#: ../pykolab/cli/cmd_list_user_subscriptions.py:88 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:98 #, python-format msgid "No unsubscribed folders for user %s" msgstr "" +#: ../pykolab/cli/cmd_mailbox_cleanup.py:37 +msgid "Clean up mailboxes that do no longer have an owner." +msgstr "" + +#: ../pykolab/cli/cmd_mailbox_cleanup.py:61 +#, python-format +msgid "Deleting folder 'user/%s'" +msgstr "" + #: ../pykolab/cli/cmd_remove_mailaddress.py:49 msgid "Invalid or unqualified email address." msgstr "" @@ -811,23 +1074,23 @@ msgstr "" #: ../pykolab/cli/cmd_remove_user_subscription.py:92 #, python-format -msgid "User %s not be unsubscribed from any folders." +msgid "User %s was not unsubscribed from any folders." msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:48 +#: ../pykolab/cli/cmd_rename_mailbox.py:52 msgid "No target mailbox name specified" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:50 +#: ../pykolab/cli/cmd_rename_mailbox.py:54 msgid "No source mailbox name specified" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:62 +#: ../pykolab/cli/cmd_rename_mailbox.py:66 #, python-format msgid "Source folder %r does not exist" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:66 +#: ../pykolab/cli/cmd_rename_mailbox.py:70 #, python-format msgid "Target folder %r already exists" msgstr "" @@ -838,17 +1101,75 @@ msgstr "" msgid "ACI Permissions" msgstr "" -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:47 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:51 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:56 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:45 +msgid "Set annotation as user USER" +msgstr "" + +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:59 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:63 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:68 msgid "Metadata value" msgstr "" -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:50 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:55 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:62 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67 msgid "Metadata path" msgstr "" +#: ../pykolab/cli/cmd_set_quota.py:43 ../pykolab/cli/cmd_set_quota.py:47 +msgid "New quota" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:44 +msgid "Delete mailboxes for recipients that do not appear to exist in LDAP." +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:50 +msgid "Display changes, do not apply them." +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:88 +#, python-format +msgid "Domains in IMAP not in LDAP: %r" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:101 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:142 +#, python-format +msgid "" +"No recipients for '%s' (would have deleted the mailbox if not for --dry-" +"run)!" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:106 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:147 +#, python-format +msgid "Deleting mailbox '%s' because it has no recipients" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:110 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:151 +#, python-format +msgid "An error occurred removing mailbox %r: %r" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:112 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:153 +#, python-format +msgid "Not automatically deleting shared folder '%s'" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:114 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:155 +#, python-format +msgid "No recipients for '%s' (use --delete to delete)!" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:136 +#, python-format +msgid "Multiple recipients for '%s'!" +msgstr "" + #: ../pykolab/cli/cmd_sync.py:41 #, python-format msgid "Found %d domains in %d seconds" @@ -859,15 +1180,23 @@ msgstr "%d Domains in %d Sekunden gefunden" msgid "Running for domain %s" msgstr "Starte für Domain %s" -#: ../pykolab/cli/cmd_sync.py:57 +#: ../pykolab/cli/cmd_sync.py:58 #, python-format msgid "Synchronizing users for %s took %d seconds" msgstr "Benutzer für %s zu synchronisieren dauerte %d Sekunden" -#: ../pykolab/cli/cmd_undelete_mailbox.py:33 +#: ../pykolab/cli/cmd_undelete_mailbox.py:39 +msgid "Do not actually execute, but state what would have been executed." +msgstr "" + +#: ../pykolab/cli/cmd_undelete_mailbox.py:42 msgid "Recover mailboxes previously deleted." msgstr "" +#: ../pykolab/cli/cmd_user_info.py:39 +msgid "Email address" +msgstr "E-Mail-Adresse" + #. This is a nested command #. This is a nested component #: ../pykolab/cli/commands.py:98 ../pykolab/setup/components.py:90 @@ -875,25 +1204,102 @@ msgstr "" msgid "Command Group: %s" msgstr "" -#: ../pykolab/cli/commands.py:109 ../pykolab/cli/commands.py:114 +#: ../pykolab/cli/commands.py:113 ../pykolab/cli/commands.py:118 msgid "No such command." msgstr "Dieses Kommando existiert nicht." -#: ../pykolab/cli/commands.py:165 ../pykolab/setup/components.py:231 +#: ../pykolab/cli/commands.py:168 ../pykolab/setup/components.py:231 #, python-format msgid "Command '%s' already registered" msgstr "" -#: ../pykolab/cli/commands.py:190 ../pykolab/setup/components.py:257 -#: ../wallace/modules.py:338 +#: ../pykolab/cli/commands.py:193 ../pykolab/setup/components.py:257 +#: ../wallace/modules.py:369 #, python-format msgid "Alias for %s" msgstr "Alias für %s" -#: ../pykolab/cli/commands.py:198 ../pykolab/setup/components.py:265 +#: ../pykolab/cli/commands.py:201 ../pykolab/setup/components.py:265 msgid "Not yet implemented" msgstr "Diese Funktion ist noch nicht implementiert" +#: ../pykolab/cli/sieve/cmd_list.py:43 ../pykolab/cli/sieve/cmd_put.py:42 +#: ../pykolab/cli/sieve/cmd_refresh.py:44 ../pykolab/cli/sieve/cmd_test.py:43 +msgid "Email Address" +msgstr "E-mail-Adresse" + +#: ../pykolab/cli/sieve/cmd_refresh.py:99 +#: ../pykolab/plugins/sievemgmt/__init__.py:111 +#, python-format +msgid "Found the following scripts for user %s: %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:100 +#: ../pykolab/plugins/sievemgmt/__init__.py:112 +#, python-format +msgid "And the following script is active for user %s: %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:178 +#: ../pykolab/plugins/sievemgmt/__init__.py:190 +#, python-format +msgid "" +"Delivery to folder active, but no folder name attribute available for user " +"%r" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:181 +#: ../pykolab/plugins/sievemgmt/__init__.py:193 +msgid "Delivery to folder active, but no folder name attribute configured" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:359 +#, python-format +msgid "MANAGEMENT script for user %s contents: %r" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:364 +#: ../pykolab/plugins/sievemgmt/__init__.py:374 +#, python-format +msgid "Uploading script MANAGEMENT failed for user %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:366 +#: ../pykolab/plugins/sievemgmt/__init__.py:376 +#, python-format +msgid "Uploading script MANAGEMENT for user %s succeeded" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:377 +#: ../pykolab/plugins/sievemgmt/__init__.py:387 +#, python-format +msgid "Including script %s in USER (for user %s)" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:386 +#: ../pykolab/plugins/sievemgmt/__init__.py:396 +#, python-format +msgid "Uploading script USER failed for user %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:388 +#: ../pykolab/plugins/sievemgmt/__init__.py:398 +#, python-format +msgid "Uploading script USER for user %s succeeded" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:416 +#: ../pykolab/plugins/sievemgmt/__init__.py:426 +#, python-format +msgid "Uploading script MASTER failed for user %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:418 +#: ../pykolab/plugins/sievemgmt/__init__.py:428 +#, python-format +msgid "Uploading script MASTER for user %s succeeded" +msgstr "" + #: ../pykolab/cli/telemetry/cmd_examine_command_issue.py:40 msgid "Unspecified command issue identifier" msgstr "" @@ -958,27 +1364,27 @@ msgstr "" #: ../pykolab/conf/__init__.py:87 #, python-format msgid "Setting %s to %r (from defaults)" -msgstr "" +msgstr "Setze %s auf %r (aus den Standardeinstellungen)" #: ../pykolab/conf/__init__.py:106 #, python-format msgid "Setting %s to %r (from CLI, verified)" -msgstr "" +msgstr "Setze %s auf %r (von der Kommandozeile, überprüft)" #: ../pykolab/conf/__init__.py:109 #, python-format msgid "Setting %s to %r (from CLI, not checked)" -msgstr "" +msgstr "Setze %s auf %r (von der Kommandozeile, nicht überprüft)" #: ../pykolab/conf/__init__.py:150 ../pykolab/conf/__init__.py:207 #, python-format msgid "Setting %s_%s to '****' (from configuration file)" -msgstr "" +msgstr "Setze %s_%s auf '****' (aus Konfigurationsdatei)" #: ../pykolab/conf/__init__.py:152 ../pykolab/conf/__init__.py:209 #, python-format msgid "Setting %s_%s to %r (from configuration file)" -msgstr "" +msgstr "Setze %s_%s auf %r (aus Konfigurationsdatei)" #: ../pykolab/conf/__init__.py:162 msgid "Setting options from configuration file" @@ -1039,76 +1445,76 @@ msgstr "Ja auf alle Fragen." msgid "No command supplied" msgstr "Kein Befehl angegeben" -#: ../pykolab/conf/__init__.py:411 +#: ../pykolab/conf/__init__.py:416 msgid "Insufficient options. Need section, key and value -in that order." msgstr "Unzureichende Optionen. Brauche Sektion, Schlüssel und Wert in dieser Reihenfolge." -#: ../pykolab/conf/__init__.py:414 +#: ../pykolab/conf/__init__.py:419 #, python-format msgid "No section '%s' exists." msgstr "Es existiert keine Sektion '%s'." -#: ../pykolab/conf/__init__.py:445 +#: ../pykolab/conf/__init__.py:461 #, python-format msgid "Setting %s to %r (from the default values for CLI options)" -msgstr "" +msgstr "Setze %s auf %r (aus den Standardwerten für Kommandozeilenoptionen)" -#: ../pykolab/conf/__init__.py:514 +#: ../pykolab/conf/__init__.py:534 #, python-format msgid "Could not execute configuration function: %s" -msgstr "" +msgstr "Konnte die Konfigurationsfunktion nicht ausführen: %s" -#: ../pykolab/conf/__init__.py:522 +#: ../pykolab/conf/__init__.py:542 #, python-format msgid "Option %s/%s does not exist in config file %s, pulling from defaults" -msgstr "" +msgstr "Die Option %s/%s existiert in der Konfigurationsdatei %s nicht, sie wird aus den Standardeinstellungen geholt" -#: ../pykolab/conf/__init__.py:530 ../pykolab/conf/__init__.py:533 +#: ../pykolab/conf/__init__.py:550 ../pykolab/conf/__init__.py:553 msgid "Option does not exist in defaults." msgstr "Diese Option hat keinen Standardwert." -#: ../pykolab/conf/__init__.py:543 +#: ../pykolab/conf/__init__.py:563 #, python-format msgid "Configuration file %s not readable." msgstr "Konfigurationsdatei %s ist nicht lesbar." -#: ../pykolab/conf/__init__.py:546 +#: ../pykolab/conf/__init__.py:566 #, python-format msgid "Configuration file %s does not exist." msgstr "Konfigurationsdatei %s existiert nicht." -#: ../pykolab/conf/__init__.py:551 +#: ../pykolab/conf/__init__.py:571 msgid "" "WARNING: A negative debug level value does not make this program be any more" " silent." msgstr "Warnung: Eine negative Fehlerprotokollierungszahl macht dieses Programm nicht noch stiller." -#: ../pykolab/conf/__init__.py:557 +#: ../pykolab/conf/__init__.py:577 msgid "This program has 9 levels of verbosity. Using the maximum of 9." msgstr "Dieses Programm hat 9 Ebenen der Detailliertheit. Benutze das Maximum 9." -#: ../pykolab/conf/__init__.py:565 ../pykolab/conf/__init__.py:571 +#: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591 msgid "Cannot start SASL authentication daemon" msgstr "Konnte SASL Authentisierungsdaemon nicht starten" -#: ../pykolab/conf/__init__.py:582 +#: ../pykolab/conf/__init__.py:602 msgid "No imaplib library found." -msgstr "" +msgstr "Keine imaplib-Bibliothek gefunden." -#: ../pykolab/conf/__init__.py:592 +#: ../pykolab/conf/__init__.py:612 msgid "No LMTP class found in the smtplib library." -msgstr "" +msgstr "Keine Klasse namens LMTP in der smtplib-Bibliothek gefunden." -#: ../pykolab/conf/__init__.py:602 +#: ../pykolab/conf/__init__.py:622 msgid "No SMTP class found in the smtplib library." -msgstr "" +msgstr "Keine Klasse namens SMTP in der smtplib-Bibliothek gefunden." -#: ../pykolab/conf/__init__.py:616 +#: ../pykolab/conf/__init__.py:636 #, python-format msgid "Found you specified a specific set of items to test: %s" msgstr "" -#: ../pykolab/conf/__init__.py:624 +#: ../pykolab/conf/__init__.py:644 #, python-format msgid "Selectively selecting: %s" msgstr "" @@ -1137,263 +1543,383 @@ msgstr "389 Verzeichnisserver oder Red Hat Verzeichnisserver" msgid "OpenLDAP or compatible" msgstr "OpenLDAP oder kompatibel" -#: ../pykolab/imap/cyrus.py:79 +#: ../pykolab/imap/cyrus.py:80 #, python-format msgid "Could not connect to Cyrus IMAP server %r" -msgstr "" +msgstr "Verbindung zum Cyrus IMAP-Server %r nicht möglich" -#: ../pykolab/imap/cyrus.py:134 +#: ../pykolab/imap/cyrus.py:137 #, python-format msgid "Continuing with separator: %r" msgstr "" -#: ../pykolab/imap/cyrus.py:139 +#: ../pykolab/imap/cyrus.py:142 msgid "Detected we are running in a Murder topology" msgstr "" -#: ../pykolab/imap/cyrus.py:143 +#: ../pykolab/imap/cyrus.py:146 msgid "This system is not part of a murder topology" msgstr "" -#: ../pykolab/imap/cyrus.py:164 +#: ../pykolab/imap/cyrus.py:167 #, python-format msgid "Checking actual backend server for folder %s through annotations" msgstr "" -#: ../pykolab/imap/cyrus.py:179 +#: ../pykolab/imap/cyrus.py:172 +msgid "Possibly reproducing the find " +msgstr "" + +#: ../pykolab/imap/cyrus.py:195 #, python-format msgid "Could not get the annotations after %s tries." msgstr "" -#: ../pykolab/imap/cyrus.py:183 +#: ../pykolab/imap/cyrus.py:199 #, python-format msgid "No annotations for %s: %r" msgstr "" -#: ../pykolab/imap/cyrus.py:190 +#: ../pykolab/imap/cyrus.py:206 #, python-format msgid "Server for INBOX folder %s is %s" msgstr "" -#: ../pykolab/imap/cyrus.py:202 +#: ../pykolab/imap/cyrus.py:226 #, python-format msgid "Setting quota for folder %s to %s" msgstr "" -#: ../pykolab/imap/cyrus.py:206 +#: ../pykolab/imap/cyrus.py:230 #, python-format msgid "Could not set quota for mailfolder %s" msgstr "" -#: ../pykolab/imap/cyrus.py:215 +#: ../pykolab/imap/cyrus.py:239 #, python-format msgid "Moving INBOX folder %s to %s" -msgstr "" +msgstr "Verschiebe Eingangsordner %s nach %s" -#: ../pykolab/imap/cyrus.py:227 +#: ../pykolab/imap/cyrus.py:254 #, python-format msgid "Setting annotation %s on folder %s" msgstr "" -#: ../pykolab/imap/cyrus.py:234 +#: ../pykolab/imap/cyrus.py:259 #, python-format msgid "Could not set annotation %r on mail folder %r: %r" msgstr "" -#: ../pykolab/imap/cyrus.py:238 +#: ../pykolab/imap/cyrus.py:263 #, python-format msgid "Transferring folder %s from %s to %s" msgstr "" -#: ../pykolab/imap/cyrus.py:298 +#: ../pykolab/imap/cyrus.py:323 #, python-format msgid "Undeleting %s to %s" msgstr "" -#: ../pykolab/imap/__init__.py:45 +#: ../pykolab/imap/cyrus.py:334 +#, python-format +msgid "Would have transfered %s from %s to %s" +msgstr "" + +#: ../pykolab/imap/cyrus.py:336 +#, python-format +msgid "Would have renamed %s to %s" +msgstr "" + +#: ../pykolab/imap/__init__.py:46 #, python-format msgid "Cleaning up ACL entries for %s across all folders" msgstr "" -#: ../pykolab/imap/__init__.py:60 +#: ../pykolab/imap/__init__.py:61 #, python-format msgid "Cleaning up ACL entries referring to identifier %s" msgstr "" -#: ../pykolab/imap/__init__.py:69 +#: ../pykolab/imap/__init__.py:70 #, python-format msgid "Iterating over %d folders" msgstr "" #. Set the ACL to '' (effectively deleting the ACL entry) -#: ../pykolab/imap/__init__.py:82 +#: ../pykolab/imap/__init__.py:83 #, python-format msgid "Removing acl %r for subject %r from folder %r" msgstr "" -#: ../pykolab/imap/__init__.py:143 +#: ../pykolab/imap/__init__.py:145 +msgid "No administrator password is available." +msgstr "" + +#: ../pykolab/imap/__init__.py:153 #, python-format msgid "Logging on to Cyrus IMAP server %s" -msgstr "" +msgstr "Anmeldung am Cyrus IMAP Server %s" -#: ../pykolab/imap/__init__.py:152 +#: ../pykolab/imap/__init__.py:162 #, python-format msgid "Logging on to Dovecot IMAP server %s" -msgstr "" +msgstr "Anmeldung am Dovecot IMAP Server %s" -#: ../pykolab/imap/__init__.py:161 +#: ../pykolab/imap/__init__.py:171 #, python-format msgid "Logging on to generic IMAP server %s" -msgstr "" +msgstr "Anmeldung am generischen IMAP Server %s" -#: ../pykolab/imap/__init__.py:179 +#: ../pykolab/imap/__init__.py:189 #, python-format msgid "Reusing existing IMAP server connection to %s" msgstr "Benutze Verbindung zum IMAP Server %s wieder" -#: ../pykolab/imap/__init__.py:181 +#: ../pykolab/imap/__init__.py:191 #, python-format msgid "Reconnecting to IMAP server %s" msgstr "Verbinde nochmal zum IMAP Server %s" -#: ../pykolab/imap/__init__.py:197 +#: ../pykolab/imap/__init__.py:208 msgid "Called imap.disconnect() on a server that we had no connection to." msgstr "" -#: ../pykolab/imap/__init__.py:212 +#: ../pykolab/imap/__init__.py:222 ../pykolab/imap/__init__.py:234 #, python-format -msgid "%r has no attribute %s" -msgstr "%r hat kein Attribut %s" +msgid "Could not create folder %r" +msgstr "" -#: ../pykolab/imap/__init__.py:285 -msgid "Private annotations need to be set using the appropriate user account." +#: ../pykolab/imap/__init__.py:223 +#, python-format +msgid " on server %r" msgstr "" -#: ../pykolab/imap/__init__.py:318 ../pykolab/imap/__init__.py:353 +#: ../pykolab/imap/__init__.py:244 ../pykolab/imap/__init__.py:246 +#, python-format +msgid "%r has no attribute %s" +msgstr "%r hat kein Attribut %s" + +#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428 #, python-format msgid "Creating new shared folder %s" msgstr "Erzeuge einen neuen geteilten Ordner %s" -#: ../pykolab/imap/__init__.py:375 +#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675 +#, python-format +msgid "Downcasing mailbox name %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:457 #, python-format msgid "Creating new mailbox for user %s" msgstr "Erzeuge eine neue Mailbox für Benutzer %s" -#: ../pykolab/imap/__init__.py:404 +#: ../pykolab/imap/__init__.py:470 +msgid "Waiting for the Cyrus IMAP Murder to settle..." +msgstr "" + +#: ../pykolab/imap/__init__.py:516 #, python-format msgid "Creating additional folders for user %s" msgstr "Erzeuge weitere Order für Benutzer %s" -#: ../pykolab/imap/__init__.py:428 +#: ../pykolab/imap/__init__.py:535 +#, python-format +msgid "Waiting for the Cyrus murder to settle... %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:547 +#, python-format +msgid "Correcting additional folder name from %r to %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:553 #, python-format msgid "Mailbox already exists: %s" msgstr "Mailbox existiert bereits: %s" -#: ../pykolab/imap/__init__.py:471 +#: ../pykolab/imap/__init__.py:593 msgid "Subscribing user to the additional folders" msgstr "Abonniere weitere Ordner für den Benutzer" -#: ../pykolab/imap/__init__.py:531 ../pykolab/imap/__init__.py:605 +#: ../pykolab/imap/__init__.py:607 +msgid "Using the following tests for folder subscriptions:" +msgstr "" + +#: ../pykolab/imap/__init__.py:609 +#, python-format +msgid " %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:612 +#, python-format +msgid "Folder %s" +msgstr "" + +#: ../pykolab/imap/__init__.py:624 +#, python-format +msgid "Subscribing %s to folder %s" +msgstr "" + +#: ../pykolab/imap/__init__.py:628 +#, python-format +msgid "Subscribing %s to folder %s failed: %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:658 +#, python-format +msgid "Could not rename %s to reside on partition %s" +msgstr "" + +#: ../pykolab/imap/__init__.py:691 +#, python-format +msgid "INBOX folder to rename (%s) does not exist" +msgstr "" + +#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770 #, python-format msgid "Renaming INBOX from %s to %s" msgstr "Benenne INBOX Ordner von %s in %s um" -#: ../pykolab/imap/__init__.py:535 +#: ../pykolab/imap/__init__.py:698 #, python-format msgid "Could not rename INBOX folder %s to %s" msgstr "Konnte INBOX Ordner nicht von %s in %s umbenennen" -#: ../pykolab/imap/__init__.py:537 ../pykolab/imap/__init__.py:609 +#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774 #, python-format msgid "" "Moving INBOX folder %s won't succeed as target folder %s already exists" +msgstr "Der INBOX-Ordner %s kann nicht verschoben werden, weil der Zielordner %s bereits existiert" + +#: ../pykolab/imap/__init__.py:704 +#, python-format +msgid "Server for mailbox %r is %r" msgstr "" -#: ../pykolab/imap/__init__.py:547 +#: ../pykolab/imap/__init__.py:712 #, python-format msgid "Looking for folder '%s', we found folders: %r" -msgstr "" +msgstr "Auf der Suche nach dem Ordner '%s' haben wir diese Ordner gefunden: %r" -#: ../pykolab/imap/__init__.py:570 +#: ../pykolab/imap/__init__.py:735 #, python-format msgid "Setting ACL rights %s for subject %s on folder " msgstr "Richte ACL Rechte %s für Subjekt %s des Ordners ein" -#: ../pykolab/imap/__init__.py:581 +#: ../pykolab/imap/__init__.py:746 #, python-format msgid "Removing ACL rights %s for subject %s on folder " msgstr "" -#: ../pykolab/imap/__init__.py:602 +#: ../pykolab/imap/__init__.py:767 #, python-format msgid "Found old INBOX folder %s" -msgstr "" +msgstr "Alter INBOX-Ordner %s gefunden" -#: ../pykolab/imap/__init__.py:611 +#: ../pykolab/imap/__init__.py:776 #, python-format msgid "Did not find old folder user/%s to rename" msgstr "Konnte alten Ordner user/%s nicht zum umbenennen finden." -#: ../pykolab/imap/__init__.py:613 +#: ../pykolab/imap/__init__.py:778 msgid "Value for user is not a dictionary" msgstr "Der Wert für user ist kein dictionary" #. TODO: Go in fact correct the quota. -#: ../pykolab/imap/__init__.py:673 +#: ../pykolab/imap/__init__.py:846 #, python-format msgid "Cannot get current IMAP quota for folder %s" msgstr "Kann aktuelles IMAP Kontingent für den Ordner %s nicht bekommen" -#: ../pykolab/imap/__init__.py:686 +#: ../pykolab/imap/__init__.py:859 #, python-format msgid "Quota for %s currently is %s" msgstr "Kontingent für %s ist aktuell %s" -#: ../pykolab/imap/__init__.py:692 +#: ../pykolab/imap/__init__.py:865 #, python-format msgid "Adjusting authentication database quota for folder %s to %d" msgstr "" -#: ../pykolab/imap/__init__.py:697 +#: ../pykolab/imap/__init__.py:870 #, python-format msgid "Correcting quota for %s to %s (currently %s)" msgstr "" -#: ../pykolab/imap/__init__.py:774 +#: ../pykolab/imap/__init__.py:947 #, python-format msgid "Checking folder: %s" msgstr "Überprüfe Ordner: %s" -#: ../pykolab/imap/__init__.py:779 +#: ../pykolab/imap/__init__.py:952 #, python-format msgid "Folder has no corresponding user (1): %s" msgstr "" -#: ../pykolab/imap/__init__.py:782 +#: ../pykolab/imap/__init__.py:955 #, python-format msgid "Folder has no corresponding user (2): %s" msgstr "" #. We got user identifier only -#: ../pykolab/imap/__init__.py:797 +#: ../pykolab/imap/__init__.py:970 msgid "Please don't give us just a user identifier" msgstr "" -#: ../pykolab/imap/__init__.py:800 +#: ../pykolab/imap/__init__.py:973 #, python-format msgid "Deleting folder %s" -msgstr "" +msgstr "Lösche Verzeichnis %s" #: ../pykolab/__init__.py:50 msgid "Returning thread local configuration" msgstr "" -#: ../pykolab/logger.py:105 +#: ../pykolab/itip/__init__.py:43 +#, python-format +msgid "Method %r not really interesting for us." +msgstr "" + +#: ../pykolab/itip/__init__.py:49 #, python-format -msgid "Could not change the ownership of log file %s" +msgid "Raw iTip payload: %s" +msgstr "" + +#: ../pykolab/itip/__init__.py:59 +msgid "Could not read iTip from message." msgstr "" -#: ../pykolab/logger.py:121 +#: ../pykolab/itip/__init__.py:67 +#, python-format +msgid "Duplicate iTip object: %s" +msgstr "" + +#: ../pykolab/itip/__init__.py:90 +msgid "iTip event without a start" +msgstr "" + +#: ../pykolab/itip/__init__.py:132 +msgid "Message is not an iTip message (non-multipart message)" +msgstr "" + +#: ../pykolab/itip/__init__.py:225 +#, python-format +msgid "Failed to compose iTip reply message: %r" +msgstr "" + +#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936 +#: ../wallace/module_resources.py:964 +#, python-format +msgid "SMTP sendmail error: %r" +msgstr "" + +#: ../pykolab/logger.py:173 ../pykolab/logger.py:179 +#, python-format +msgid "Could not change permissions on %s: %r" +msgstr "" + +#: ../pykolab/logger.py:196 #, python-format msgid "Cannot log to file %s: %s" msgstr "" @@ -1425,12 +1951,12 @@ msgstr "" #: ../pykolab/plugins/__init__.py:74 #, python-format msgid "RuntimeError for plugin %s: %s" -msgstr "" +msgstr "Laufzeitfehler von Plugin %s: %s" #: ../pykolab/plugins/__init__.py:78 #, python-format msgid "Plugin %s failed to load (%s: %s)" -msgstr "" +msgstr "Plugin %s konnte nicht geladen werden (%s: %s)" #: ../pykolab/plugins/__init__.py:116 ../pykolab/plugins/__init__.py:118 #, python-format @@ -1470,7 +1996,7 @@ msgstr "" #: ../pykolab/plugins/__init__.py:187 #, python-format msgid "Cannot check options for plugin %s: %s" -msgstr "" +msgstr "Kein Zugriff auf Einstellungen des Plugins %s: %s" #: ../pykolab/plugins/__init__.py:189 #, python-format @@ -1491,16 +2017,51 @@ msgstr "" msgid "Attribute substitution for 'mail' failed in Recipient Policy" msgstr "" -#: ../pykolab/plugins/recipientpolicy/__init__.py:115 +#: ../pykolab/plugins/recipientpolicy/__init__.py:116 msgid "Could not parse the alternative mail routines" msgstr "" +#: ../pykolab/plugins/recipientpolicy/__init__.py:120 +#, python-format +msgid "Alternative mail routines: %r" +msgstr "" + #: ../pykolab/plugins/recipientpolicy/__init__.py:127 -#: ../pykolab/plugins/recipientpolicy/__init__.py:137 +#, python-format +msgid "" +"An error occurred in composing the secondary mail attribute for entry %r" +msgstr "" + +#: ../pykolab/plugins/recipientpolicy/__init__.py:138 +#: ../pykolab/plugins/recipientpolicy/__init__.py:153 +#, python-format +msgid "Appending additional mail address: %s" +msgstr "" + +#: ../pykolab/plugins/recipientpolicy/__init__.py:142 +#, python-format +msgid "Policy for secondary email address failed: %r" +msgstr "" + +#: ../pykolab/plugins/recipientpolicy/__init__.py:157 msgid "" "Attribute substitution for 'alternative_mail' failed in Recipient Policy" msgstr "" +#: ../pykolab/plugins/roundcubedb/__init__.py:48 +#, python-format +msgid "user_delete: %r" +msgstr "" + +#: ../pykolab/plugins/roundcubedb/__init__.py:55 +#: ../pykolab/setup/setup_roundcube.py:160 +msgid "Roundcube installation path not found." +msgstr "" + +#: ../pykolab/plugins/sievemgmt/__init__.py:51 +msgid "Wrong number of arguments for sieve management plugin" +msgstr "" + #: ../pykolab/setup/components.py:58 msgid "Display this help." msgstr "Zeige diese Hilfe." @@ -1521,47 +2082,27 @@ msgstr "" msgid "Free/Busy is not installed on this system" msgstr "" -#: ../pykolab/setup/setup_freebusy.py:55 -msgid "" -"\n" -" Please supply the MySQL password for the 'roundcube'\n" -" user. You have supplied this password earlier, and it is\n" -" available from the database URI setting in\n" -" /etc/roundcubemail/db.inc.php.\n" -" " -msgstr "" - -#: ../pykolab/setup/setup_freebusy.py:64 -#: ../pykolab/setup/setup_roundcube.py:56 -msgid "MySQL roundcube password" -msgstr "MySQL roundcube Passwort" +#: ../pykolab/setup/setup_imap.py:45 +msgid "Setup IMAP." +msgstr "Richte IMAP ein." -#: ../pykolab/setup/setup_freebusy.py:92 -#: ../pykolab/setup/setup_roundcube.py:116 ../pykolab/setup/setup_zpush.py:71 -#, python-format -msgid "Using template file %r" +#: ../pykolab/setup/setup_imap.py:89 +msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf" msgstr "" -#: ../pykolab/setup/setup_freebusy.py:99 -#: ../pykolab/setup/setup_roundcube.py:123 ../pykolab/setup/setup_zpush.py:78 -#, python-format -msgid "Successfully compiled template %r, writing out to %r" +#: ../pykolab/setup/setup_imap.py:114 +msgid "Could not write out Cyrus IMAP configuration file /etc/cyrus.conf" msgstr "" -#: ../pykolab/setup/setup_freebusy.py:119 ../pykolab/setup/setup_imap.py:143 -#: ../pykolab/setup/setup_ldap.py:288 ../pykolab/setup/setup_ldap.py:521 -#: ../pykolab/setup/setup_mta.py:309 ../pykolab/setup/setup_mysql.py:49 -#: ../pykolab/setup/setup_roundcube.py:191 -#: ../pykolab/setup/setup_syncroton.py:66 ../pykolab/setup/setup_zpush.py:98 -msgid "Could not start and configure to start on boot, the " +#: ../pykolab/setup/setup_imap.py:158 +msgid "Could not start the cyrus-imapd and kolab-saslauthd services." msgstr "" -#: ../pykolab/setup/setup_imap.py:44 -msgid "Setup IMAP." -msgstr "Richte IMAP ein." - -#: ../pykolab/setup/setup_imap.py:88 ../pykolab/setup/setup_imap.py:113 -msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf" +#: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:81 +#: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:455 +#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:237 +#: ../pykolab/setup/setup_syncroton.py:102 +msgid "Could not configure to start on boot, the " msgstr "" #: ../pykolab/setup/setup_kolabd.py:43 @@ -1577,23 +2118,62 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:43 +#: ../pykolab/setup/setup_kolabd.py:72 +msgid "Could not start the kolab server service." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:45 msgid "LDAP Options" msgstr "LDAP Optionen" -#: ../pykolab/setup/setup_ldap.py:50 +#: ../pykolab/setup/setup_ldap.py:52 msgid "Specify FQDN (overriding defaults)." msgstr "" -#: ../pykolab/setup/setup_ldap.py:58 +#: ../pykolab/setup/setup_ldap.py:60 msgid "Allow anonymous binds (default: no)." msgstr "" -#: ../pykolab/setup/setup_ldap.py:62 +#: ../pykolab/setup/setup_ldap.py:68 +msgid "Skip setting up the LDAP server." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:76 +msgid "Setup configuration for OpenLDAP compatibility." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:84 +msgid "Setup configuration for Active Directory compatibility." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:88 msgid "Setup LDAP." msgstr "LDAP Einrichten" -#: ../pykolab/setup/setup_ldap.py:74 +#: ../pykolab/setup/setup_ldap.py:97 +msgid "Skipping setup of LDAP, as specified" +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:126 +msgid "" +"\n" +" You can not configure Kolab to run against OpenLDAP\n" +" and Active Directory simultaneously.\n" +" " +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:139 +msgid "" +"\n" +" It seems 389 Directory Server has an existing\n" +" instance configured. This setup script does not\n" +" intend to destroy or overwrite your data. Please\n" +" make sure /etc/dirsrv/ and /var/lib/dirsrv/ are\n" +" clean so that this setup does not have to worry.\n" +" " +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:154 msgid "" "\n" " Please supply a password for the LDAP administrator user\n" @@ -1602,11 +2182,11 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:82 +#: ../pykolab/setup/setup_ldap.py:162 msgid "Administrator password" msgstr "Administrator Passwort" -#: ../pykolab/setup/setup_ldap.py:89 +#: ../pykolab/setup/setup_ldap.py:169 msgid "" "\n" " Please supply a password for the LDAP Directory Manager\n" @@ -1616,11 +2196,11 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:98 +#: ../pykolab/setup/setup_ldap.py:178 msgid "Directory Manager password" msgstr "Verzeichnismanager Passwort" -#: ../pykolab/setup/setup_ldap.py:105 +#: ../pykolab/setup/setup_ldap.py:185 msgid "" "\n" " Please choose the system user and group the service\n" @@ -1629,15 +2209,15 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:112 +#: ../pykolab/setup/setup_ldap.py:195 ../pykolab/setup/setup_ldap.py:198 msgid "User" msgstr "Benutzer" -#: ../pykolab/setup/setup_ldap.py:113 +#: ../pykolab/setup/setup_ldap.py:196 ../pykolab/setup/setup_ldap.py:199 msgid "Group" msgstr "Gruppe" -#: ../pykolab/setup/setup_ldap.py:143 +#: ../pykolab/setup/setup_ldap.py:234 msgid "" "\n" " This setup procedure plans to set up Kolab Groupware for\n" @@ -1648,18 +2228,18 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:157 +#: ../pykolab/setup/setup_ldap.py:248 msgid "Domain name to use" msgstr "" -#: ../pykolab/setup/setup_ldap.py:162 ../pykolab/setup/setup_ldap.py:187 +#: ../pykolab/setup/setup_ldap.py:253 ../pykolab/setup/setup_ldap.py:278 msgid "" "\n" " Invalid input. Please try again.\n" " " -msgstr "" +msgstr "\n Ungültige Eingabe. Bitte nochmals versuchen.\n " -#: ../pykolab/setup/setup_ldap.py:171 +#: ../pykolab/setup/setup_ldap.py:262 msgid "" "\n" " The standard root dn we composed for you follows. Please\n" @@ -1667,11 +2247,15 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:182 +#: ../pykolab/setup/setup_ldap.py:273 msgid "Root DN to use" msgstr "" -#: ../pykolab/setup/setup_ldap.py:234 +#: ../pykolab/setup/setup_ldap.py:325 +msgid "No directory server setup tool available." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:337 msgid "" "\n" " Setup is now going to set up the 389 Directory Server. This\n" @@ -1680,24 +2264,42 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:241 +#: ../pykolab/setup/setup_ldap.py:344 msgid "Setting up 389 Directory Server" msgstr "Richte 389 Verzeichnisserver ein" -#. TODO: Get the return code and display output if not successful. -#: ../pykolab/setup/setup_ldap.py:253 +#: ../pykolab/setup/setup_ldap.py:356 +msgid "" +"\n" +" An error was detected in the setup procedure for 389\n" +" Directory Server. This setup will write out stderr and\n" +" stdout to /var/log/kolab/setup.error.log and\n" +" /var/log/kolab/setup.out.log respectively, before it\n" +" exits.\n" +" " +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:373 msgid "Setup DS stdout:" msgstr "" -#: ../pykolab/setup/setup_ldap.py:256 +#: ../pykolab/setup/setup_ldap.py:376 msgid "Setup DS stderr:" msgstr "" -#: ../pykolab/setup/setup_ldap.py:279 ../pykolab/setup/setup_mysql.py:129 -msgid "Could not find the Kolab schema file" -msgstr "Konnte Kolab Schema Datei nicht finden" +#: ../pykolab/setup/setup_ldap.py:402 +msgid "Could not copy the LDAP extensions for Kolab" +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:405 +msgid "Could not find the ldap Kolab schema file" +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:417 +msgid "Could not start the directory server service." +msgstr "" -#: ../pykolab/setup/setup_ldap.py:293 +#: ../pykolab/setup/setup_ldap.py:431 msgid "" "\n" " Please supply a Cyrus Administrator password. This\n" @@ -1708,11 +2310,11 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:303 +#: ../pykolab/setup/setup_ldap.py:441 msgid "Cyrus Administrator password" msgstr "Cyrus Administrator Passwort" -#: ../pykolab/setup/setup_ldap.py:310 +#: ../pykolab/setup/setup_ldap.py:448 msgid "" "\n" " Please supply a Kolab Service account password. This\n" @@ -1722,98 +2324,127 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:319 +#: ../pykolab/setup/setup_ldap.py:457 msgid "Kolab Service password" msgstr "Kolab-Dienst-Passwort" -#: ../pykolab/setup/setup_ldap.py:329 +#: ../pykolab/setup/setup_ldap.py:467 msgid "Writing out configuration to kolab.conf" msgstr "Schreibe Einstellungen in kolab.conf" -#: ../pykolab/setup/setup_ldap.py:343 +#: ../pykolab/setup/setup_ldap.py:481 msgid "Inserting service users into LDAP." msgstr "Füge Service-Benutzer ins LDAP ein." -#: ../pykolab/setup/setup_ldap.py:417 +#: ../pykolab/setup/setup_ldap.py:555 msgid "Writing out cn=kolab,cn=config" msgstr "Schreibe cn=kolab,cn=config" #. TODO: Add kolab-admin role #. TODO: Assign kolab-admin admin ACLs -#: ../pykolab/setup/setup_ldap.py:441 +#: ../pykolab/setup/setup_ldap.py:579 #, python-format msgid "Adding domain %s to list of domains for this deployment" msgstr "Füge Domain %s zu dieser Installation hinzu" -#: ../pykolab/setup/setup_ldap.py:457 +#: ../pykolab/setup/setup_ldap.py:607 msgid "Disabling anonymous binds" msgstr "Stelle anonymes Binden ab" #. TODO: Ensure the uid attribute is unique #. TODO^2: Consider renaming the general "attribute uniqueness to "uid #. attribute uniqueness" -#: ../pykolab/setup/setup_ldap.py:465 +#: ../pykolab/setup/setup_ldap.py:615 msgid "Enabling attribute uniqueness plugin" msgstr "" -#: ../pykolab/setup/setup_ldap.py:471 +#: ../pykolab/setup/setup_ldap.py:621 msgid "Enabling referential integrity plugin" msgstr "" -#: ../pykolab/setup/setup_ldap.py:477 +#: ../pykolab/setup/setup_ldap.py:627 msgid "Enabling and configuring account policy plugin" msgstr "" #. TODO: Add kolab-admin role -#: ../pykolab/setup/setup_ldap.py:492 +#: ../pykolab/setup/setup_ldap.py:642 msgid "Adding the kolab-admin role" msgstr "Füge Kolab-Admin Rolle hinzu" #. TODO: User writeable attributes on root_dn -#: ../pykolab/setup/setup_ldap.py:503 +#: ../pykolab/setup/setup_ldap.py:653 #, python-format msgid "Setting access control to %s" msgstr "" -#: ../pykolab/setup/setup_mta.py:40 +#: ../pykolab/setup/setup_ldap.py:679 +msgid "Could not start and configure to start on boot, the " +msgstr "" + +#: ../pykolab/setup/setup_mta.py:41 msgid "Setup MTA." msgstr "Richte MTA ein." -#: ../pykolab/setup/setup_mta.py:224 ../pykolab/setup/setup_php.py:80 +#: ../pykolab/setup/setup_mta.py:317 ../pykolab/setup/setup_php.py:106 #, python-format msgid "Setting key %r to %r" msgstr "" -#: ../pykolab/setup/setup_mta.py:252 +#: ../pykolab/setup/setup_mta.py:350 msgid "Could not write out Postfix configuration file /etc/postfix/master.cf" msgstr "" -#: ../pykolab/setup/setup_mta.py:287 -msgid "" -"Could not write out Amavis configuration file /etc/amavisd/amavisd.conf" +#: ../pykolab/setup/setup_mta.py:397 +msgid "Could not write out Amavis configuration file amavisd.conf" +msgstr "" + +#: ../pykolab/setup/setup_mta.py:405 +msgid "Not writing out any configuration for Amavis." +msgstr "" + +#: ../pykolab/setup/setup_mta.py:437 +msgid "Could not start the postfix, clamav and amavisd services services." msgstr "" #: ../pykolab/setup/setup_mysql.py:39 msgid "Setup MySQL." msgstr "Richte MySQL ein." -#: ../pykolab/setup/setup_mysql.py:53 +#: ../pykolab/setup/setup_mysql.py:49 +msgid "Could not start the MySQL database service." +msgstr "" + +#: ../pykolab/setup/setup_mysql.py:71 +msgid "What MySQL server are we setting up?" +msgstr "" + +#: ../pykolab/setup/setup_mysql.py:75 msgid "" "\n" -" Please supply a root password for MySQL. This password will\n" -" be the administrative user for this MySQL server, and it\n" -" should be kept a secret. After this setup process has\n" -" completed, Kolab is going to discard and forget about this\n" -" password, but you will need it for administrative tasks in\n" -" MySQL.\n" -" " +" Please supply the root password for MySQL, so we can set\n" +" up user accounts for other components that use MySQL.\n" +" " msgstr "" -#: ../pykolab/setup/setup_mysql.py:64 +#: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99 +#: ../pykolab/setup/setup_roundcube.py:183 +#: ../pykolab/setup/setup_syncroton.py:63 msgid "MySQL root password" +msgstr "MySQL root Password" + +#: ../pykolab/setup/setup_mysql.py:88 +msgid "" +"\n" +" Please supply a root password for MySQL. This password\n" +" will be the administrative user for this MySQL server,\n" +" and it should be kept a secret. After this setup process\n" +" has completed, Kolab is going to discard and forget\n" +" about this password, but you will need it for\n" +" administrative tasks in MySQL.\n" +" " msgstr "" -#: ../pykolab/setup/setup_mysql.py:103 +#: ../pykolab/setup/setup_mysql.py:139 msgid "" "\n" " Please supply a password for the MySQL user 'kolab'.\n" @@ -1822,8 +2453,12 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_mysql.py:111 +#: ../pykolab/setup/setup_mysql.py:147 msgid "MySQL kolab password" +msgstr "MySQL-kolab Password" + +#: ../pykolab/setup/setup_mysql.py:165 +msgid "Could not find the MySQL Kolab schema file" msgstr "" #: ../pykolab/setup/setup_php.py:42 @@ -1832,23 +2467,38 @@ msgstr "PHP Optionen" #: ../pykolab/setup/setup_php.py:49 msgid "Specify the timezone for PHP." +msgstr "Zeitzone für PHP definieren." + +#: ../pykolab/setup/setup_php.py:57 +msgid "Specify the path to the php.ini file used with the webserver." msgstr "" -#: ../pykolab/setup/setup_php.py:53 +#: ../pykolab/setup/setup_php.py:61 msgid "Setup PHP." msgstr "Richte PHP ein." -#: ../pykolab/setup/setup_php.py:58 +#: ../pykolab/setup/setup_php.py:66 msgid "" "\n" " Please supply the timezone PHP should be using.\n" +" You have to use a Continent or Country / City locality name\n" +" like 'Europe/Berlin', but not just 'CEST'.\n" " " msgstr "" -#: ../pykolab/setup/setup_php.py:64 +#: ../pykolab/setup/setup_php.py:74 msgid "Timezone ID" msgstr "Zeitzonen ID" +#: ../pykolab/setup/setup_php.py:80 +#, python-format +msgid "Cannot configure PHP through %r (No such file or directory)" +msgstr "" + +#: ../pykolab/setup/setup_php.py:91 +msgid "Could not find PHP configuration file php.ini" +msgstr "" + #: ../pykolab/setup/setup_roundcube.py:44 msgid "Setup Roundcube." msgstr "Stelle Roundcube ein." @@ -1862,18 +2512,29 @@ msgid "" " " msgstr "" -#: ../pykolab/setup/setup_syncroton.py:40 -msgid "Setup Syncroton." -msgstr "Richte Syncroton ein." +#: ../pykolab/setup/setup_roundcube.py:56 +msgid "MySQL roundcube password" +msgstr "MySQL roundcube Passwort" -#: ../pykolab/setup/setup_zpush.py:41 -msgid "Setup zpush." -msgstr "zpush einrichten." +#: ../pykolab/setup/setup_roundcube.py:120 +#, python-format +msgid "Using template file %r" +msgstr "" -#: ../pykolab/setup/setup_zpush.py:45 -msgid "Z-Push is not installed on this system" +#: ../pykolab/setup/setup_roundcube.py:127 +#, python-format +msgid "Successfully compiled template %r, writing out to %r" +msgstr "" + +#: ../pykolab/setup/setup_roundcube.py:228 +#: ../pykolab/setup/setup_syncroton.py:93 +msgid "Could not start the webserver server service." msgstr "" +#: ../pykolab/setup/setup_syncroton.py:40 +msgid "Setup Syncroton." +msgstr "Richte Syncroton ein." + #. start_max = (int)(time.time()) #: ../pykolab/telemetry.py:588 #, python-format @@ -1899,111 +2560,181 @@ msgstr "" msgid "No database available" msgstr "Keine Datenbank verfügbar" -#: ../pykolab/utils.py:57 ../pykolab/utils.py:59 +#: ../pykolab/utils.py:62 ../pykolab/utils.py:64 #, python-format msgid "Confirm %s: " -msgstr "" +msgstr "Bestätige %s:" -#: ../pykolab/utils.py:62 +#: ../pykolab/utils.py:67 msgid "Incorrect confirmation. " -msgstr "" +msgstr "Ungültige Bestätigung." -#: ../pykolab/utils.py:67 ../pykolab/utils.py:72 +#: ../pykolab/utils.py:72 ../pykolab/utils.py:77 #, python-format msgid "%s: " msgstr "%s:" -#: ../pykolab/utils.py:69 ../pykolab/utils.py:74 +#: ../pykolab/utils.py:74 ../pykolab/utils.py:79 #, python-format msgid "%s [%s]: " msgstr "%s [%s]: " -#: ../pykolab/utils.py:119 +#: ../pykolab/utils.py:124 msgid "Please answer 'yes' or 'no'." msgstr "Bitte antworten Sie mit 'yes' (ja) oder 'no' (nein)." -#: ../pykolab/utils.py:185 +#: ../pykolab/utils.py:164 +msgid "Choice" +msgstr "" + +#: ../pykolab/utils.py:167 +msgid "Choice (type '?' for options)" +msgstr "" + +#: ../pykolab/utils.py:268 #, python-format msgid "Could not change the permissions on %s" msgstr "" -#: ../pykolab/wap_client/__init__.py:257 +#: ../pykolab/utils.py:479 +#, python-format +msgid "Transliterating string %r with locale %r" +msgstr "" + +#: ../pykolab/utils.py:487 +msgid "Attempting to set locale" +msgstr "" + +#: ../pykolab/utils.py:489 +msgid "Success setting locale" +msgstr "" + +#: ../pykolab/utils.py:491 +msgid "Failure to set locale" +msgstr "" + +#: ../pykolab/utils.py:499 +#, python-format +msgid "Executing '%s | %s'" +msgstr "" + +#: ../pykolab/utils.py:510 +#, python-format +msgid "Could not translate %s using locale %s" +msgstr "" + +#: ../pykolab/wap_client/__init__.py:320 #, python-format msgid "Requesting %r with params %r" msgstr "" -#: ../pykolab/wap_client/__init__.py:263 +#: ../pykolab/wap_client/__init__.py:328 #, python-format msgid "Got response: %r" msgstr "" #. Some data is not JSON -#: ../pykolab/wap_client/__init__.py:268 +#: ../pykolab/wap_client/__init__.py:334 msgid "Response data is not JSON" msgstr "" -#: ../pykolab/xml/attendee.py:79 ../pykolab/xml/attendee.py:99 +#. support integer values, too +#: ../pykolab/xml/attendee.py:9 ../pykolab/xml/attendee.py:17 +msgid "Needs Action" +msgstr "" + +#: ../pykolab/xml/attendee.py:10 ../pykolab/xml/attendee.py:18 +msgid "Accepted" +msgstr "Akzeptiert" + +#: ../pykolab/xml/attendee.py:11 ../pykolab/xml/attendee.py:19 +msgid "Declined" +msgstr "Abgelehnt" + +#: ../pykolab/xml/attendee.py:12 ../pykolab/xml/attendee.py:20 +msgid "Tentatively Accepted" +msgstr "Provisorisch Akzeptiert" + +#: ../pykolab/xml/attendee.py:13 ../pykolab/xml/attendee.py:21 +msgid "Delegated" +msgstr "Delegiert" + +#: ../pykolab/xml/attendee.py:14 +msgid "Completed" +msgstr "" + +#: ../pykolab/xml/attendee.py:15 +msgid "In Process" +msgstr "" + +#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130 msgid "Not a valid attendee" msgstr "" -#: ../pykolab/xml/attendee.py:84 +#: ../pykolab/xml/attendee.py:115 msgid "No valid delegator references found" msgstr "" -#: ../pykolab/xml/attendee.py:104 +#: ../pykolab/xml/attendee.py:135 msgid "No valid delegatee references found" msgstr "" -#: ../pykolab/xml/attendee.py:140 +#: ../pykolab/xml/attendee.py:180 #, python-format msgid "Invalid cutype %r" msgstr "" -#: ../pykolab/xml/attendee.py:151 +#: ../pykolab/xml/attendee.py:192 #, python-format msgid "Invalid participant status %r" msgstr "" -#: ../pykolab/xml/attendee.py:159 +#: ../pykolab/xml/attendee.py:200 #, python-format msgid "Invalid role %r" msgstr "" -#: ../pykolab/xml/event.py:172 +#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708 +#: ../pykolab/xml/event.py:751 +msgid "Event start needs datetime.date or datetime.datetime instance" +msgstr "" + +#: ../pykolab/xml/event.py:241 #, python-format msgid "No attendee with email or name %r" msgstr "" -#: ../pykolab/xml/event.py:180 +#: ../pykolab/xml/event.py:249 #, python-format msgid "Invalid argument value attendee %r, must be basestring or Attendee" msgstr "" -#: ../pykolab/xml/event.py:186 +#: ../pykolab/xml/event.py:255 #, python-format msgid "No attendee with email %r" msgstr "" -#: ../pykolab/xml/event.py:192 +#: ../pykolab/xml/event.py:261 #, python-format msgid "No attendee with name %r" msgstr "" -#: ../pykolab/xml/event.py:338 +#: ../pykolab/xml/event.py:426 msgid "Invalid participant status" msgstr "" -#: ../pykolab/xml/event.py:538 -msgid "Event end needs datetime.date or datetime.datetime instance" +#: ../pykolab/xml/event.py:542 +#, python-format +msgid "Invalid status %r" msgstr "" -#: ../pykolab/xml/event.py:654 +#: ../pykolab/xml/event.py:550 #, python-format -msgid "Invalid status %r" +msgid "Invalid classification %r" msgstr "" -#: ../pykolab/xml/event.py:675 ../pykolab/xml/event.py:725 -msgid "Event start needs datetime.date or datetime.datetime instance" +#: ../pykolab/xml/event.py:577 +msgid "Event end needs datetime.date or datetime.datetime instance" msgstr "" #: ../pykolab/xml/event.py:761 @@ -2011,62 +2742,434 @@ msgstr "" msgid "Invalid status set: %r" msgstr "" -#: ../pykolab/xml/event.py:879 +#: ../pykolab/xml/event.py:923 msgid "No sender specified" msgstr "" -#: ../saslauthd/__init__.py:126 ../saslauthd/__init__.py:134 -#: ../wallace/__init__.py:362 ../wallace/__init__.py:371 +#: ../pykolab/xml/event.py:932 +#, python-format +msgid "Invitation for %s was %s" +msgstr "" + +#: ../pykolab/xml/event.py:937 +msgid "This is an automated response to one of your event requests." +msgstr "" + +#: ../saslauthd/__init__.py:99 +#, python-format +msgid "Could not create %r: %r" +msgstr "" + +#: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145 +#: ../wallace/__init__.py:403 ../wallace/__init__.py:412 msgid "" "Traceback occurred, please report a bug at http://bugzilla.kolabsys.com" msgstr "Ein Fehler mit Traceback trat auf, bitte legen Sie einen Bericht auf http://bugzilla.kolabsys.com an" -#: ../wallace/__init__.py:61 +#: ../saslauthd/__init__.py:185 +msgid "kolab-saslauthd could not accept " +msgstr "" + +#: ../saslauthd/__init__.py:190 +msgid "Maximum tries exceeded, exiting" +msgstr "" + +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190 +#: ../wallace/module_resources.py:879 +#, python-format +msgid "Reservation Request for %(summary)s was %(status)s" +msgstr "" + +#. check notification message sent to resource owner (jane) +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:605 +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:621 +#: ../wallace/module_resources.py:954 +#, python-format +msgid "Booking for %s has been %s" +msgstr "Buchung für %s wurde %s" + +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:146 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:720 +#: ../wallace/module_invitationpolicy.py:374 +#, python-format +msgid "\"%(summary)s\" has been %(status)s" +msgstr "\"%(summary)s\" wurde %(status)s" + +#. check for notification message +#. this notification should be suppressed until mark has replied, too +#. this triggers an additional notification +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:616 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:622 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:635 +#: ../wallace/module_invitationpolicy.py:925 +#, python-format +msgid "\"%s\" has been updated" +msgstr "\"%s\" wurde aktualisiert" + +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:627 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639 +msgid "PENDING" +msgstr "" + +#: ../wallace/__init__.py:57 +#, python-format +msgid "Wallace modules: %r" +msgstr "" + +#: ../wallace/__init__.py:69 +#, python-format +msgid "Module %s.execute() failed on message %r with error: %s" +msgstr "" + +#: ../wallace/__init__.py:78 #, python-format msgid "Worker process %s initializing" msgstr "" -#: ../wallace/__init__.py:80 +#: ../wallace/__init__.py:100 msgid "Bind address for Wallace." -msgstr "" +msgstr "Bind-Adresse für Wallace." -#: ../wallace/__init__.py:106 +#: ../wallace/__init__.py:126 msgid "Port that Wallace is supposed to use." msgstr "Port, den Wallace benutzen soll." -#: ../wallace/__init__.py:157 +#: ../wallace/__init__.py:177 #, python-format msgid "Could not bind to socket on port %d on bind " msgstr "" -#: ../wallace/__init__.py:169 +#: ../wallace/__init__.py:189 msgid "Could not shut down socket" -msgstr "" +msgstr "Konnte Socket nicht schließen" -#: ../wallace/__init__.py:226 +#: ../wallace/__init__.py:253 msgid "Accepted connection" msgstr "Verbindung akzeptiert" -#: ../wallace/__init__.py:387 +#: ../wallace/__init__.py:428 #, python-format msgid "Could not write pid file %s" msgstr "" -#: ../wallace/module_optout.py:61 ../wallace/module_resources.py:94 +#: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60 +#: ../wallace/module_invitationpolicy.py:168 ../wallace/module_optout.py:61 +#: ../wallace/module_resources.py:120 #, python-format msgid "Issuing callback after processing to stage %s" msgstr "" -#: ../wallace/module_optout.py:62 ../wallace/module_resources.py:100 +#: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61 +#: ../wallace/module_invitationpolicy.py:170 ../wallace/module_optout.py:62 +#: ../wallace/module_resources.py:126 #, python-format msgid "Testing cb_action_%s()" msgstr "" -#: ../wallace/module_optout.py:64 ../wallace/module_resources.py:103 +#: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63 +#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:64 +#: ../wallace/module_resources.py:129 #, python-format msgid "Attempting to execute cb_action_%s()" msgstr "" +#: ../wallace/module_footer.py:67 +#, python-format +msgid "Executing module footer for %r, %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:66 +#, python-format +msgid "Executing module gpgencrypt for %r, %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:98 +msgid "Message is already encrypted (app/pgp-enc content-type)" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:102 +msgid "Message already encrypted by main content-type header" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:131 +msgid "" +"Configured to encrypt to a key not configured, and strict policy enabled. " +"Bailing out." +msgstr "" + +#: ../wallace/module_gpgencrypt.py:134 +msgid "" +"Configured to encrypt to a key not configured, but continuing anyway (see " +"'gpgencrypt_strict')." +msgstr "" + +#: ../wallace/module_gpgencrypt.py:171 +#, python-format +msgid "Recipients: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:183 +#, python-format +msgid "Current keys: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:188 +#, python-format +msgid "Retrieving key for recipient: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:192 ../wallace/module_gpgencrypt.py:208 +#, python-format +msgid "Found matching address %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:200 +#, python-format +msgid "Found matching address %r in remote keys" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:232 +#, python-format +msgid "An error occurred: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:154 +#, python-format +msgid "Invitation policy called for %r, %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:211 +#: ../wallace/module_resources.py:169 +#, python-format +msgid "Failed to parse iTip events from message: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:215 +msgid "" +"Message is not an iTip message or does not contain any (valid) iTip events." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:219 +#, python-format +msgid "" +"iTip events attached to this message contain the following information: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:232 +#, python-format +msgid "No itips, no users, pass along %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:235 +#, python-format +msgid "iTips, but no users, pass along %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:255 +#, python-format +msgid "No user attendee matching envelope recipient %s, skip message" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:259 +#, python-format +msgid "Receiving user: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:284 +#, python-format +msgid "Apply invitation policy %r for domain %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:295 +#, python-format +msgid "Ignoring '%s' iTip method" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:299 +#, python-format +msgid "iTip message %r consumed by the invitationpolicy module" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:315 +msgid "Pass invitation for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:320 +#, python-format +msgid "Receiving Attendee: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:339 +#, python-format +msgid "Existing event: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:350 +#, python-format +msgid "Precondition for event %r fulfilled: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:386 +#, python-format +msgid "No RSVP for recipient %r requested" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:412 +msgid "Pass reply for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:419 +#, python-format +msgid "Sender Attendee: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:431 +#, python-format +msgid "" +"The iTip reply sequence (%r) doesn't match the referred event version (%r). " +"Forwarding to Inbox." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:437 +#, python-format +msgid "Auto-updating event %r on iTip REPLY" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:459 +#: ../wallace/module_invitationpolicy.py:488 +msgid "" +"The event referred by this reply was not found in the user's calendars. " +"Forwarding to Inbox." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:472 +msgid "Pass cancellation for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:517 +#, python-format +msgid "Checking if email address %r belongs to a local user" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:522 +#, python-format +msgid "User DN: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:524 +#, python-format +msgid "No user record(s) found for %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:577 +#, python-format +msgid "User record doesn't have the mailbox attribute %r set" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:590 +#, python-format +msgid "IMAP proxy authentication failed: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:612 +#, python-format +msgid "List calendar folders for user %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:628 +#, python-format +msgid "IMAP metadata for %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:658 +#, python-format +msgid "Searching folder %r for event %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:670 +#: ../wallace/module_invitationpolicy.py:709 +#: ../wallace/module_resources.py:486 +#, python-format +msgid "Failed to parse event from message %s/%s: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:696 +#, python-format +msgid "Listing events from folder %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:715 +#, python-format +msgid "Existing event %r conflicts with invitation %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:722 +#: ../wallace/module_resources.py:344 +#, python-format +msgid "start: %r, end: %r, total: %r, messages: %d" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:748 +#, python-format +msgid "%r is locked, waiting..." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:811 +#, python-format +msgid "Failed to save event: no calendar folder found for user %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:814 +#, python-format +msgid "Save event %r to user calendar %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:827 +#, python-format +msgid "Failed to save event to user calendar at %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:843 +#, python-format +msgid "Delete event %r in %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:863 +#, python-format +msgid "Compose participation status summary for event %r to user %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:901 +#, python-format +msgid "" +"Waiting for more automated replies (got %d of %d); skipping notification" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:998 +#, python-format +msgid "Updated %s's copy of %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1001 +#, python-format +msgid "Attendee %s's copy of %r not found" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1004 +#, python-format +msgid "Attendee %r not found in LDAP" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1008 +#, python-format +msgid "" +"\n" +" %(name)s has %(status)s your invitation for %(summary)s.\n" +"\n" +" *** This is an automated response sent by the Kolab Invitation system ***\n" +" " +msgstr "" + #. modules.next_module('optout') #: ../wallace/module_optout.py:70 #, python-format @@ -2088,210 +3191,295 @@ msgstr "" msgid "Could not send request to optout_url %s" msgstr "" -#: ../wallace/module_resources.py:81 +#: ../wallace/module_resources.py:110 #, python-format msgid "Resource Management called for %r, %r" msgstr "" -#: ../wallace/module_resources.py:139 +#: ../wallace/module_resources.py:174 msgid "Message is not an iTip message or does not contain any " msgstr "" -#: ../wallace/module_resources.py:147 +#: ../wallace/module_resources.py:182 msgid "iTip events attached to this message contain the " msgstr "" -#: ../wallace/module_resources.py:174 +#: ../wallace/module_resources.py:205 msgid "Not an iTip message, but sent to resource nonetheless. Reject message" msgstr "" -#: ../wallace/module_resources.py:182 -msgid "No itips, no resources, pass along" +#: ../wallace/module_resources.py:213 +#, python-format +msgid "No itips, no resources, pass along %r" msgstr "" -#: ../wallace/module_resources.py:186 -msgid "iTips, but no resources, pass along" +#: ../wallace/module_resources.py:216 +#, python-format +msgid "iTips, but no resources, pass along %r" msgstr "" -#: ../wallace/module_resources.py:218 +#: ../wallace/module_resources.py:225 #, python-format -msgid "Resources: %r" +msgid "No resource attendees matching envelope recipient %s, Reject message" msgstr "" -#: ../wallace/module_resources.py:236 +#: ../wallace/module_resources.py:234 #, python-format -msgid "Checking events in resource folder %r" +msgid "Resources: %r; %r" msgstr "" -#: ../wallace/module_resources.py:243 +#: ../wallace/module_resources.py:244 #, python-format -msgid "Mailbox for resource %r doesn't exist" +msgid "Receiving Resource: %r; %r" msgstr "" -#: ../wallace/module_resources.py:256 +#: ../wallace/module_resources.py:252 #, python-format -msgid "Fetching message UID %r from folder %r" +msgid "Recipient %r is non-participant, ignoring message" msgstr "" -#: ../wallace/module_resources.py:295 +#: ../wallace/module_resources.py:279 #, python-format -msgid "Event %r conflicts with event " +msgid "Accept invitation for individual resource %r / %r" msgstr "" #: ../wallace/module_resources.py:308 #, python-format -msgid "start: %r, end: %r, total: %r, messages: %r" +msgid "Delegate invitation for resource collection %r to %r" msgstr "" -#: ../wallace/module_resources.py:315 +#: ../wallace/module_resources.py:340 +#, python-format +msgid "Failed to read resource calendar for %r: %r" +msgstr "" + +#: ../wallace/module_resources.py:350 #, python-format msgid "Polling for resource %r" msgstr "" -#: ../wallace/module_resources.py:319 +#: ../wallace/module_resources.py:353 #, python-format msgid "Resource %r has been popped from the list" msgstr "" -#: ../wallace/module_resources.py:326 +#: ../wallace/module_resources.py:357 msgid "Resource is a collection" msgstr "" -#: ../wallace/module_resources.py:374 ../wallace/module_resources.py:424 +#: ../wallace/module_resources.py:368 #, python-format -msgid "Adding event to %r" +msgid "Removed conflicting resources from %r: (%r) => %r" msgstr "" -#: ../wallace/module_resources.py:473 +#: ../wallace/module_resources.py:380 #, python-format -msgid "Method %r not really interesting for us." +msgid "Conflicting events: %r for resource %r" msgstr "" -#: ../wallace/module_resources.py:481 +#: ../wallace/module_resources.py:397 #, python-format -msgid "Raw iTip payload: %s" +msgid "Delegate to another resource collection member: %r to %r" msgstr "" -#: ../wallace/module_resources.py:491 -msgid "Could not read iTip from message." +#: ../wallace/module_resources.py:459 +#, python-format +msgid "Checking events in resource folder %r" msgstr "" -#: ../wallace/module_resources.py:513 -msgid "iTip event without a start" +#: ../wallace/module_resources.py:475 +#, python-format +msgid "Fetching message UID %r from folder %r" msgstr "" -#. end if c.name == "VEVENT" -#. end for c in cal.walk() -#. end if part.get_content_type() == "text/calendar" -#. end for part in message.walk() -#. if message.is_multipart() -#: ../wallace/module_resources.py:543 -msgid "Message is not an iTip message (non-multipart message)" +#: ../wallace/module_resources.py:498 +#, python-format +msgid "Event %r conflicts with event %r" msgstr "" -#: ../wallace/module_resources.py:564 +#: ../wallace/module_resources.py:525 #, python-format -msgid "Checking if email address %r belongs to a resource (collection)" +msgid "Adding event to %r: %r" msgstr "" -#: ../wallace/module_resources.py:575 ../wallace/module_resources.py:649 -#: ../wallace/module_resources.py:699 +#: ../wallace/module_resources.py:573 #, python-format -msgid "No resource (collection) records found for %r" +msgid "Failed to save event to resource calendar at %r: %r" +msgstr "" + +#: ../wallace/module_resources.py:590 +#, python-format +msgid "Delete resource calendar object %r in %r: %r" msgstr "" -#: ../wallace/module_resources.py:583 ../wallace/module_resources.py:657 -#: ../wallace/module_resources.py:707 +#: ../wallace/module_resources.py:633 +#, python-format +msgid "Checking if email address %r belongs to a resource (collection)" +msgstr "" + +#: ../wallace/module_resources.py:641 ../wallace/module_resources.py:709 +#: ../wallace/module_resources.py:743 #, python-format msgid "Resource record(s): %r" msgstr "" -#: ../wallace/module_resources.py:589 ../wallace/module_resources.py:664 -#: ../wallace/module_resources.py:714 +#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711 +#: ../wallace/module_resources.py:746 +#, python-format +msgid "No resource (collection) records found for %r" +msgstr "" + +#: ../wallace/module_resources.py:647 ../wallace/module_resources.py:715 +#: ../wallace/module_resources.py:750 #, python-format msgid "Resource record: %r" msgstr "" -#: ../wallace/module_resources.py:608 +#: ../wallace/module_resources.py:667 #, python-format msgid "Raw itip_events: %r" msgstr "" -#: ../wallace/module_resources.py:616 +#: ../wallace/module_resources.py:675 #, python-format msgid "Raw set of attendees: %r" msgstr "" -#: ../wallace/module_resources.py:624 +#: ../wallace/module_resources.py:683 #, python-format msgid "Raw set of resources: %r" msgstr "" -#: ../wallace/module_resources.py:638 +#: ../wallace/module_resources.py:702 #, python-format msgid "Checking if attendee %r is a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:671 ../wallace/module_resources.py:717 +#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752 msgid "Resource reservation made but no resource records found" msgstr "" -#: ../wallace/module_resources.py:689 +#: ../wallace/module_resources.py:737 #, python-format msgid "Checking if resource %r is a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:721 +#: ../wallace/module_resources.py:755 msgid "The following resources are being referred to in the " msgstr "" +#: ../wallace/module_resources.py:894 +#, python-format +msgid "" +"\n" +" *** This is an automated response, please do not reply! ***\n" +"\n" +" Your reservation was delegated to \"%s\" which is available for the requested time.\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:905 +#, python-format +msgid "" +"\n" +" *** This is an automated response, please do not reply! ***\n" +" \n" +" We hereby inform you that your reservation was %s.\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:912 +#, python-format +msgid "" +"\n" +" If you have questions about this reservation, please contact\n" +" %s <%s> %s\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:941 +#, python-format +msgid "Sending booking notification for event %r to %r from %r" +msgstr "" + +#: ../wallace/module_resources.py:954 +msgid "failed" +msgstr "" + +#: ../wallace/module_resources.py:973 +#, python-format +msgid "" +"\n" +" The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s.\n" +"\n" +" *** This is an automated message, sent to you as the resource owner. ***\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:979 +#, python-format +msgid "" +"\n" +" A reservation request for %(resource)s could not be processed automatically.\n" +" Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s.\n" +"\n" +" *** This is an automated message, sent to you as the resource owner. ***\n" +" " +msgstr "" + #. This is a nested module -#: ../wallace/modules.py:96 +#: ../wallace/modules.py:97 #, python-format msgid "Module Group: %s" +msgstr "Modulgruppe: %s" + +#: ../wallace/modules.py:108 +#, python-format +msgid "No such module %r in modules %r (1)." msgstr "" -#: ../wallace/modules.py:107 ../wallace/modules.py:112 -msgid "No such module." +#: ../wallace/modules.py:113 +#, python-format +msgid "No such module %r in modules %r (2)." msgstr "" -#: ../wallace/modules.py:118 +#: ../wallace/modules.py:119 #, python-format msgid "Holding message in queue for manual review (%s by %s)" -msgstr "" +msgstr "Behalte Nachricht zur manuellen Prüfung in der Warteliste (%s von %s)" -#: ../wallace/modules.py:121 +#: ../wallace/modules.py:122 #, python-format msgid "Deferring message in %s (by module %s)" msgstr "" -#: ../wallace/modules.py:131 +#: ../wallace/modules.py:134 #, python-format msgid "The time when the message was sent: %r" -msgstr "" +msgstr "Zeitpunkt des Versandts: %r" -#: ../wallace/modules.py:132 +#: ../wallace/modules.py:135 #, python-format msgid "The time now: %r" msgstr "Die Zeit ist jetzt: %r" -#: ../wallace/modules.py:133 +#: ../wallace/modules.py:136 #, python-format msgid "The time delta: %r" -msgstr "" +msgstr "Die Zeitdifferenz: %r" #. TODO: Send NDR back to user -#: ../wallace/modules.py:137 +#: ../wallace/modules.py:140 #, python-format msgid "Message in file %s older then 5 days, deleting" msgstr "" -#: ../wallace/modules.py:162 +#: ../wallace/modules.py:165 #, python-format msgid "Rejecting message in %s (by module %s)" msgstr "" -#: ../wallace/modules.py:180 +#: ../wallace/modules.py:186 #, python-format msgid "" "This is the email system Wallace at %s.\n" @@ -2304,21 +3492,31 @@ msgid "" "Your message is being delivered to any other recipients you may have\n" "sent your message to. There is no need to resend the message to those\n" "recipients.\n" -msgstr "" +msgstr "Ich bin das E-Mail-System Wallace auf %s.\n\nMit Bedauern muß ich Sie informieren, daß die angehängte Nachricht\nnicht an die folgenden Empfänger zugestellt werden konnte:\n\n- %s\n\nFalls Sie noch andere Empfänger angegeben haben wurde die Nachricht\nan diese zugestellt. An diese Empfänger müssen sie die Nachricht\nnicht erneut senden.\n" -#: ../wallace/modules.py:195 +#: ../wallace/modules.py:201 #, python-format msgid "" "X-Wallace-Module: %s\n" "X-Wallace-Result: REJECT\n" msgstr "" -#: ../wallace/modules.py:248 +#: ../wallace/modules.py:260 #, python-format msgid "Accepting message in %s (by module %s)" msgstr "" -#: ../wallace/modules.py:316 +#: ../wallace/modules.py:262 #, python-format -msgid "Module '%s' already registered" +msgid "Accepting message in: %r" msgstr "" + +#: ../wallace/modules.py:269 +#, python-format +msgid "recipients: %r" +msgstr "" + +#: ../wallace/modules.py:347 +#, python-format +msgid "Module '%s' already registered" +msgstr "Modul '%s' ist bereits registriert" diff --git a/po/de_DE.po b/po/de_DE.po index 03f16f2..b8bcc6f 100644 --- a/po/de_DE.po +++ b/po/de_DE.po @@ -1,678 +1,913 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: -# <grote@kolabsys.com>, 2012. +# Christoph Wickert <christoph.wickert@gmail.com>, 2012 +# Grote <grote@kolabsys.com>, 2012 +# Thomas Brüderli <roundcube@gmail.com>, 2014 msgid "" msgstr "" "Project-Id-Version: Kolab Groupware Solution\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-01-12 11:17+0000\n" -"PO-Revision-Date: 2012-08-14 11:13+0000\n" -"Last-Translator: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com>\n" -"Language-Team: German (Germany) (http://www.transifex.com/projects/p/kolab/" -"language/de_DE/)\n" -"Language: de_DE\n" +"POT-Creation-Date: 2014-07-10 07:21-0400\n" +"PO-Revision-Date: 2014-07-22 13:04+0000\n" +"Last-Translator: Thomas Brüderli <roundcube@gmail.com>\n" +"Language-Team: German (Germany) (http://www.transifex.com/projects/p/kolab/language/de_DE/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"Language: de_DE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: ../bin/kolab_smtp_access_policy.py:206 +#: ../bin/kolab_smtp_access_policy.py:209 #, python-format msgid "Adding policy request to instance %s" msgstr "Füge Richtlinien-Anfrage zu Instanz %s hinzu" -#: ../bin/kolab_smtp_access_policy.py:446 +#: ../bin/kolab_smtp_access_policy.py:479 msgid "Unauthorized access not allowed" msgstr "Unberechtigter Zugriff nicht erlaubt" -#: ../bin/kolab_smtp_access_policy.py:475 -#: ../bin/kolab_smtp_access_policy.py:657 +#: ../bin/kolab_smtp_access_policy.py:508 +#: ../bin/kolab_smtp_access_policy.py:689 msgid "Could not find recipient" msgstr "Konnte den Empfänger nicht finden" -#: ../bin/kolab_smtp_access_policy.py:494 -#: ../bin/kolab_smtp_access_policy.py:594 +#: ../bin/kolab_smtp_access_policy.py:527 #, python-format -msgid "Could not find envelope sender user %s" -msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden" +msgid "Could not find envelope sender user %s (511)" +msgstr "" -#: ../bin/kolab_smtp_access_policy.py:537 +#: ../bin/kolab_smtp_access_policy.py:570 #, python-format msgid "Obtained authenticated user details for %r: %r" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:616 +#: ../bin/kolab_smtp_access_policy.py:627 +#, python-format +msgid "Could not find envelope sender user %s" +msgstr "Konnte den Absender-Umschlag für den Benutzer %s nicht finden" + +#: ../bin/kolab_smtp_access_policy.py:649 #, python-format msgid "%s is unauthorized to send on behalf of %s" msgstr "Benutzer %s ist nicht berechtigt als Benutzer %s zu senden" -#: ../bin/kolab_smtp_access_policy.py:626 +#: ../bin/kolab_smtp_access_policy.py:659 #, python-format -msgid "User %s attempted to use envelope sender address %s " +msgid "" +"User %s attempted to use envelope sender address %s without authorization" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:681 -#: ../bin/kolab_smtp_access_policy.py:692 +#: ../bin/kolab_smtp_access_policy.py:713 +#: ../bin/kolab_smtp_access_policy.py:724 #, python-format msgid "Found user %s to be a delegate user of %s" msgstr "Benutzer %s ist ein delegierter Benutzer von %s" -#: ../bin/kolab_smtp_access_policy.py:716 +#: ../bin/kolab_smtp_access_policy.py:748 #, python-format -msgid "Verifying authenticated sender '%(sender)s' with " +msgid "" +"Verifying authenticated sender '%(sender)s' with sasl_username " +"'%(sasl_username)s' for recipient '%(recipient)s'" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:721 +#: ../bin/kolab_smtp_access_policy.py:751 #, python-format -msgid "Verifying unauthenticated sender '%(sender)s' " +msgid "" +"Verifying unauthenticated sender '%(sender)s' for recipient '%(recipient)s'" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:738 +#: ../bin/kolab_smtp_access_policy.py:767 #, python-format -msgid "Reproducing verify_recipient(%s, %s) from " +msgid "Reproducing verify_recipient(%s, %s) from cache" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:760 +#: ../bin/kolab_smtp_access_policy.py:804 #, python-format msgid "Using authentication domain %s instead of %s" msgstr "Benutze Authentisierungsdomain %s anstelle von %s" -#: ../bin/kolab_smtp_access_policy.py:770 +#: ../bin/kolab_smtp_access_policy.py:814 #, python-format msgid "Domain %s is a primary domain" msgstr "Die Domain %s ist die primäre Domain" -#: ../bin/kolab_smtp_access_policy.py:778 +#: ../bin/kolab_smtp_access_policy.py:822 #, python-format -msgid "Checking the recipient for domain %s that is not " +msgid "" +"Checking the recipient for domain %s that is not ours. This is probably a " +"configuration error." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:794 -msgid "This recipient address is related to multiple " +#: ../bin/kolab_smtp_access_policy.py:837 +msgid "" +"This recipient address is related to multiple object entries and the SMTP " +"Access Policy can therefore not restrict message flow" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:803 +#: ../bin/kolab_smtp_access_policy.py:854 #, python-format -msgid "Recipient address %r not found. Allowing since " +msgid "" +"Recipient address %r not found. Allowing since the MTA was configured to " +"accept the recipient." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:831 +#: ../bin/kolab_smtp_access_policy.py:890 msgid "Invalid recipient" msgstr "Ungültiger Empfänger" -#: ../bin/kolab_smtp_access_policy.py:842 +#: ../bin/kolab_smtp_access_policy.py:901 msgid "Could not find this user, accepting" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:905 -#: ../bin/kolab_smtp_access_policy.py:958 +#: ../bin/kolab_smtp_access_policy.py:974 +#: ../bin/kolab_smtp_access_policy.py:1050 #, python-format -msgid "Sender %s is not allowed to send to " +msgid "Sender %s is not allowed to send to recipient %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:945 +#: ../bin/kolab_smtp_access_policy.py:1038 #, python-format -msgid "Reproducing verify_sender(%r) from cache, " +msgid "Reproducing verify_sender(%r) from cache" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:974 +#: ../bin/kolab_smtp_access_policy.py:1055 +msgid "Unverifiable sender." +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1060 +msgid "Sender is not using an alias" +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1068 msgid "Sender uses unauthorized envelope sender address" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:991 +#: ../bin/kolab_smtp_access_policy.py:1085 msgid "Could not verify sender" msgstr "Konnte den Absender nicht verifizieren" -#: ../bin/kolab_smtp_access_policy.py:998 -msgid "Verifying whether sender is allowed to send to " +#: ../bin/kolab_smtp_access_policy.py:1092 +msgid "" +"Verifying whether sender is allowed to send to recipient using sender policy" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1012 +#: ../bin/kolab_smtp_access_policy.py:1105 #, python-format msgid "Result is %r" msgstr "Das Ergebnis ist %r" -#: ../bin/kolab_smtp_access_policy.py:1017 +#: ../bin/kolab_smtp_access_policy.py:1110 msgid "No recipient policy restrictions exist for this sender" msgstr "Es existiert keine Empfängerrichtlinie für diesen Absender" -#: ../bin/kolab_smtp_access_policy.py:1026 +#: ../bin/kolab_smtp_access_policy.py:1119 msgid "Found a recipient policy to apply for this sender." msgstr "Empfänger-Richtlinie für diesen Benutzer gefunden" -#: ../bin/kolab_smtp_access_policy.py:1041 +#: ../bin/kolab_smtp_access_policy.py:1134 #, python-format -msgid "Sender %s not allowed to send to recipient " +msgid "Sender %s not allowed to send to recipient %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1063 +#: ../bin/kolab_smtp_access_policy.py:1155 msgid "Cleaning up the cache" msgstr "Aufräumen des Cache" -#: ../bin/kolab_smtp_access_policy.py:1085 +#: ../bin/kolab_smtp_access_policy.py:1177 msgid "" "The 'uri' setting in the kolab_smtp_access_policy section is soon going to " "be deprecated in favor of 'cache_uri'" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1101 +#: ../bin/kolab_smtp_access_policy.py:1193 #, python-format msgid "Operational Error in caching: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1152 +#: ../bin/kolab_smtp_access_policy.py:1245 #, python-format msgid "Caching the policy result with timestamp %d" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1229 +#: ../bin/kolab_smtp_access_policy.py:1319 #, python-format msgid "Returning action DEFER_IF_PERMIT: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1234 +#: ../bin/kolab_smtp_access_policy.py:1324 #, python-format msgid "Returning action DUNNO: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1239 +#: ../bin/kolab_smtp_access_policy.py:1329 #, python-format msgid "Returning action HOLD: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1244 +#: ../bin/kolab_smtp_access_policy.py:1334 #, python-format msgid "Returning action PERMIT: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1249 +#: ../bin/kolab_smtp_access_policy.py:1459 #, python-format msgid "Returning action REJECT: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1298 +#: ../bin/kolab_smtp_access_policy.py:1505 msgid "Starting to loop for new request" msgstr "Starte Schleife für neue Anfrage" -#: ../bin/kolab_smtp_access_policy.py:1305 +#: ../bin/kolab_smtp_access_policy.py:1512 msgid "Timeout for policy request reading exceeded" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1311 +#: ../bin/kolab_smtp_access_policy.py:1518 msgid "End of current request" msgstr "Ende der aktuellen Anfrage" -#: ../bin/kolab_smtp_access_policy.py:1315 +#: ../bin/kolab_smtp_access_policy.py:1522 #, python-format msgid "Getting line: %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1319 +#: ../bin/kolab_smtp_access_policy.py:1526 msgid "Returning request" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1348 +#: ../bin/kolab_smtp_access_policy.py:1555 msgid "Access Policy Options" msgstr "Zugriffsrichtlinien-Einstellungen" -#: ../bin/kolab_smtp_access_policy.py:1355 +#: ../bin/kolab_smtp_access_policy.py:1562 msgid "SMTP Policy request timeout." msgstr "Zeitüberschreitung der SMTP Richtlinien-Anfrage" -#: ../bin/kolab_smtp_access_policy.py:1361 +#: ../bin/kolab_smtp_access_policy.py:1568 msgid "Verify the recipient access policy." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1367 +#: ../bin/kolab_smtp_access_policy.py:1574 msgid "Verify the sender access policy." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1373 +#: ../bin/kolab_smtp_access_policy.py:1580 msgid "Allow unauthenticated senders." msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1385 +#: ../bin/kolab_smtp_access_policy.py:1594 #, python-format msgid "Got request instance %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1394 +#: ../bin/kolab_smtp_access_policy.py:1603 #, python-format msgid "Request instance %s is in state %s" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1402 +#: ../bin/kolab_smtp_access_policy.py:1611 #, python-format msgid "Request instance %s is not yet in DATA state" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1414 +#: ../bin/kolab_smtp_access_policy.py:1623 #, python-format msgid "Request instance %s reached DATA state" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1432 +#: ../bin/kolab_smtp_access_policy.py:1643 +#, python-format +msgid "Unhandled exception caught: %r" +msgstr "" + +#: ../bin/kolab_smtp_access_policy.py:1647 msgid "Sender access denied" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1434 +#: ../bin/kolab_smtp_access_policy.py:1649 msgid "Recipient access denied" msgstr "" -#: ../bin/kolab_smtp_access_policy.py:1436 +#: ../bin/kolab_smtp_access_policy.py:1651 msgid "No objections" msgstr "" -#: ../conf.py:37 ../kolab.py:34 ../saslauthd.py:33 +#: ../conf.py:37 ../kolab-cli.py:34 ../saslauthd.py:33 msgid "Cannot load pykolab/logger.py:" msgstr "" -#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:48 -#: ../wallace/__init__.py:66 +#: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:51 +#: ../wallace/__init__.py:85 msgid "Daemon Options" msgstr "" -#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:55 -#: ../wallace/__init__.py:73 +#: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:58 +#: ../wallace/__init__.py:92 msgid "Fork to the background." msgstr "" -#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:64 -#: ../wallace/__init__.py:99 +#: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:67 +#: ../wallace/__init__.py:118 msgid "Path to the PID file to use." msgstr "" -#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:73 -#: ../wallace/__init__.py:116 +#: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76 +#: ../wallace/__init__.py:135 msgid "Run as user USERNAME" msgstr "" -#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:83 -#: ../wallace/__init__.py:90 +#: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86 +#: ../wallace/__init__.py:109 msgid "Run as group GROUPNAME" msgstr "" -#: ../kolabd/__init__.py:122 ../pykolab/utils.py:180 -#: ../wallace/__init__.py:297 +#: ../kolabd/__init__.py:122 ../pykolab/logger.py:139 ../pykolab/utils.py:234 +#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:329 #, python-format msgid "Group %s does not exist" msgstr "" -#: ../kolabd/__init__.py:131 ../wallace/__init__.py:306 +#: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301 +#: ../wallace/__init__.py:338 #, python-format msgid "Switching real and effective group id to %d" msgstr "" -#: ../kolabd/__init__.py:153 ../pykolab/utils.py:204 -#: ../wallace/__init__.py:328 +#: ../kolabd/__init__.py:153 ../pykolab/logger.py:159 ../pykolab/utils.py:258 +#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:360 #, python-format msgid "User %s does not exist" msgstr "" -#: ../kolabd/__init__.py:163 ../wallace/__init__.py:338 +#: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333 +#: ../wallace/__init__.py:370 #, python-format msgid "Switching real and effective user id to %d" msgstr "" -#: ../kolabd/__init__.py:172 ../wallace/__init__.py:347 +#: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342 +#: ../wallace/__init__.py:379 msgid "Could not change real and effective uid and/or gid" msgstr "" -#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:122 -#: ../wallace/__init__.py:367 +#: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133 +#: ../wallace/__init__.py:399 msgid "Interrupted by user" msgstr "" #: ../kolabd/__init__.py:197 ../kolabd/__init__.py:208 -#: ../wallace/__init__.py:371 ../wallace/__init__.py:381 msgid "Traceback occurred, please report a " msgstr "" -#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:130 -#: ../wallace/__init__.py:377 +#: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141 +#: ../wallace/__init__.py:408 #, python-format msgid "Type Error: %s" msgstr "" -#: ../kolabd/__init__.py:223 ../pykolab/auth/ldap/__init__.py:1623 +#: ../kolabd/__init__.py:230 +msgid "Could not connect to LDAP, is it running?" +msgstr "" + +#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137 #: ../pykolab/cli/cmd_sync.py:36 msgid "Listing domains..." +msgstr "Domänen werden geladen…" + +#: ../kolabd/__init__.py:244 +msgid "No domains. Not syncing" msgstr "" -#: ../kolabd/__init__.py:260 +#: ../kolabd/__init__.py:275 #, python-format msgid "added domains: %r, removed domains: %r" msgstr "" -#: ../kolabd/process.py:48 +#: ../kolabd/process.py:33 +#, python-format +msgid "Process created for domain %s" +msgstr "" + +#: ../kolabd/process.py:42 #, python-format -msgid "Error in process %r, terminating: %r" +msgid "Synchronizing for domain %s" +msgstr "" + +#: ../kolabd/process.py:59 +#, python-format +msgid "" +"Error in process %r, terminating:\n" +"\t%r" msgstr "" #: ../kolabd.py:31 ../setup-kolab.py:36 ../wallace.py:31 msgid "Cannot load pykolab/constants.py:" msgstr "" -#: ../pykolab/auth/__init__.py:94 +#: ../pykolab/auth/__init__.py:89 #, python-format msgid "Called for domain %r" msgstr "" -#: ../pykolab/auth/__init__.py:107 ../pykolab/auth/__init__.py:116 +#: ../pykolab/auth/__init__.py:106 ../pykolab/auth/__init__.py:115 #, python-format msgid "Using section %s and domain %s" msgstr "" -#: ../pykolab/auth/__init__.py:121 +#: ../pykolab/auth/__init__.py:120 #, python-format msgid "Connecting to Authentication backend for domain %s" msgstr "" -#: ../pykolab/auth/__init__.py:132 +#: ../pykolab/auth/__init__.py:131 #, python-format msgid "Section %s has no option 'auth_mechanism'" msgstr "" -#: ../pykolab/auth/__init__.py:139 +#: ../pykolab/auth/__init__.py:138 #, python-format msgid "Section %s has auth_mechanism: %r" msgstr "" -#: ../pykolab/auth/__init__.py:148 ../pykolab/auth/__init__.py:157 +#: ../pykolab/auth/__init__.py:147 ../pykolab/auth/__init__.py:156 msgid "Starting LDAP..." msgstr "" -#: ../pykolab/auth/ldap/cache.py:112 +#: ../pykolab/auth/ldap/cache.py:126 #, python-format msgid "Inserting cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/cache.py:129 +#: ../pykolab/auth/ldap/cache.py:147 #, python-format msgid "Updating timestamp for cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/cache.py:136 +#: ../pykolab/auth/ldap/cache.py:155 #, python-format msgid "Updating result_attribute for cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:51 +#: ../pykolab/auth/ldap/__init__.py:52 msgid "Python LDAP library does not support persistent search" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:142 +#: ../pykolab/auth/ldap/__init__.py:143 #, python-format msgid "Attempting to authenticate user %s in realm %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:184 +#: ../pykolab/auth/ldap/__init__.py:175 ../pykolab/auth/ldap/__init__.py:226 +#, python-format +msgid "Authentication cache failed: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:216 ../pykolab/auth/ldap/__init__.py:240 #, python-format msgid "Binding with user_dn %s and password %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:194 +#: ../pykolab/auth/ldap/__init__.py:231 ../pykolab/auth/ldap/__init__.py:263 #, python-format msgid "Failed to authenticate as user %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:211 -msgid "Connecting to LDAP..." +#: ../pykolab/auth/ldap/__init__.py:249 +#, python-format +msgid "Error occured, there is no such object: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:254 +msgid "Authentication cache failed to clear entry" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:215 +#: ../pykolab/auth/ldap/__init__.py:260 +#, python-format +msgid "Exception occured: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:280 +msgid "Connecting to LDAP..." +msgstr "Zum LDAP verbinden…" + +#: ../pykolab/auth/ldap/__init__.py:284 #, python-format msgid "Attempting to use LDAP URI %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:363 +#: ../pykolab/auth/ldap/__init__.py:371 +#, python-format +msgid "Entry ID: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:373 +#, python-format +msgid "Entry DN: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:376 +#, python-format +msgid "" +"ldap search: (%r, %r, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:453 #, python-format msgid "Finding recipient with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:431 +#: ../pykolab/auth/ldap/__init__.py:529 #, python-format msgid "Finding resource with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:454 +#: ../pykolab/auth/ldap/__init__.py:560 #, python-format msgid "Using timestamp %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:486 +#: ../pykolab/auth/ldap/__init__.py:595 +msgid "Applying recipient policy disabled through configuration" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:600 #, python-format msgid "Applying recipient policy to %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:503 +#: ../pykolab/auth/ldap/__init__.py:617 #, python-format msgid "Using mail attributes: %r, with primary %r and " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:514 +#: ../pykolab/auth/ldap/__init__.py:628 #, python-format msgid "key %r not in entry" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:516 +#: ../pykolab/auth/ldap/__init__.py:630 #, python-format msgid "key %r is the prim. mail attr." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:518 +#: ../pykolab/auth/ldap/__init__.py:632 msgid "prim. mail pol. is not empty" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:521 +#: ../pykolab/auth/ldap/__init__.py:635 #, python-format msgid "key %r is the sec. mail attr." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:523 +#: ../pykolab/auth/ldap/__init__.py:637 msgid "sec. mail pol. is not empty" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:527 ../pykolab/auth/ldap/__init__.py:541 +#: ../pykolab/auth/ldap/__init__.py:641 ../pykolab/auth/ldap/__init__.py:655 #, python-format msgid "Attributes %r are not yet available for entry %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:577 +#: ../pykolab/auth/ldap/__init__.py:694 #, python-format msgid "No results for mail address %s found" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:588 +#: ../pykolab/auth/ldap/__init__.py:705 #, python-format msgid "1 result for address %s found, verifying" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:598 +#: ../pykolab/auth/ldap/__init__.py:715 #, python-format msgid "Too bad, primary email address %s " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:609 ../pykolab/auth/ldap/__init__.py:698 +#: ../pykolab/auth/ldap/__init__.py:726 ../pykolab/auth/ldap/__init__.py:815 msgid "Address assigned to us" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:664 +#: ../pykolab/auth/ldap/__init__.py:781 #, python-format msgid "No results for address %s found" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:675 +#: ../pykolab/auth/ldap/__init__.py:792 #, python-format msgid "1 result for address %s found, " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:686 +#: ../pykolab/auth/ldap/__init__.py:803 msgid "Too bad, secondary email " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:713 +#: ../pykolab/auth/ldap/__init__.py:830 msgid "Recipient policy composed the following set of secondary " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:724 +#: ../pykolab/auth/ldap/__init__.py:841 #, python-format msgid "Secondary mail addresses that we want is not None: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:735 +#: ../pykolab/auth/ldap/__init__.py:852 msgid "Avoiding the duplication of the primary mail " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:746 +#: ../pykolab/auth/ldap/__init__.py:863 #, python-format msgid "Entry is getting secondary mail addresses: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:754 +#: ../pykolab/auth/ldap/__init__.py:871 msgid "Entry did not have any secondary mail " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:780 +#: ../pykolab/auth/ldap/__init__.py:888 ../pykolab/auth/ldap/__init__.py:894 +#, python-format +msgid "secondary_mail_addresses: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:889 ../pykolab/auth/ldap/__init__.py:895 +#, python-format +msgid "entry[%s]: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:906 #, python-format msgid "Entry modifications list: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:800 +#: ../pykolab/auth/ldap/__init__.py:934 #, python-format msgid "Setting entry attribute %r to %r for %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:836 +#: ../pykolab/auth/ldap/__init__.py:970 #, python-format -msgid "Could not update dn %r" +msgid "" +"Could not update dn %r:\n" +"%r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:849 +#: ../pykolab/auth/ldap/__init__.py:983 #, python-format msgid "Using filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:887 +#: ../pykolab/auth/ldap/__init__.py:998 +#, python-format +msgid "Synchronization is searching against base DN: %s" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1044 #, python-format msgid "About to consider the user quota for %r (used: %r, " msgstr "" -#: ../pykolab/auth/ldap/__init__.py:953 -#, fuzzy -msgid "Invalid bind credentials" -msgstr "Ungültiger Empfänger" +#: ../pykolab/auth/ldap/__init__.py:1115 +msgid "Invalid DN, username and/or password." +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1236 ../pykolab/auth/ldap/__init__.py:1249 +#: ../pykolab/auth/ldap/__init__.py:1614 ../pykolab/auth/ldap/__init__.py:1627 +#, python-format +msgid "Found a subject %r with access %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1356 +#, python-format +msgid "Entry %s attribute value: %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:1364 +#, python-format +msgid "imap.user_mailbox_server(%r) result: %r" +msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1248 ../pykolab/auth/ldap/__init__.py:1372 +#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853 #, python-format msgid "Result from recipient policy: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1417 +#: ../pykolab/auth/ldap/__init__.py:1908 #, python-format msgid "Kolab user %s does not have a result attribute %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1559 +#: ../pykolab/auth/ldap/__init__.py:2067 #, python-format msgid "Finding domain root dn for domain %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1647 +#: ../pykolab/auth/ldap/__init__.py:2164 msgid "Authentication database DOWN" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1731 ../pykolab/auth/ldap/__init__.py:1766 +#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296 #, python-format msgid "Entry type: %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1854 +#: ../pykolab/auth/ldap/__init__.py:2321 +#, python-format +msgid "Done with _synchronize_callback() for entry %r" +msgstr "" + +#: ../pykolab/auth/ldap/__init__.py:2393 msgid "LDAP Search Result Data Entry:" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1870 +#: ../pykolab/auth/ldap/__init__.py:2409 msgid "Entry Change Notification attributes:" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1875 +#: ../pykolab/auth/ldap/__init__.py:2414 #, python-format msgid "Change Type: %r (%r)" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1883 +#: ../pykolab/auth/ldap/__init__.py:2422 #, python-format msgid "Previous DN: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1938 +#: ../pykolab/auth/ldap/__init__.py:2477 #, python-format msgid "Object %s searched no longer exists" -msgstr "" +msgstr "Das gesuchte Objekt %s existiert nicht mehr" -#: ../pykolab/auth/ldap/__init__.py:1948 +#: ../pykolab/auth/ldap/__init__.py:2487 #, python-format msgid "%d results..." -msgstr "" +msgstr "%d Ergebnisse…" -#: ../pykolab/auth/ldap/__init__.py:2051 +#: ../pykolab/auth/ldap/__init__.py:2590 #, python-format msgid "Searching with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2095 +#: ../pykolab/auth/ldap/__init__.py:2642 #, python-format msgid "Checking for support for %s on %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2114 +#: ../pykolab/auth/ldap/__init__.py:2661 #, python-format msgid "Found support for %s" msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:36 ../pykolab/cli/cmd_create_mailbox.py:36 +#: ../pykolab/auth/ldap/__init__.py:2706 +#, python-format +msgid "An error occured using %s: %r" +msgstr "" + +#: ../pykolab/auth/ldap/syncrepl.py:46 +msgid "The name of the persistent, unique attribute " +msgstr "" + +#: ../pykolab/cli/cmd_acl_cleanup.py:34 +msgid "Clean up ACLs that use identifiers that no longer exist" +msgstr "" + +#: ../pykolab/cli/cmd_acl_cleanup.py:56 +#, python-format +msgid "Deleting ACL %s for subject %s on folder %s" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:42 +msgid "Specify the (new) alias address" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:45 +msgid "Specify the existing recipient address" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:66 ../pykolab/cli/cmd_add_alias.py:70 +#, python-format +msgid "Domain %r is not a local domain" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:75 +msgid "Primary and secondary domain do not have the same parent domain" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:81 +#, python-format +msgid "No such recipient %r" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:87 +#, python-format +msgid "Recipient for alias %r already exists" +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:97 +msgid "Environment is not configured for " +msgstr "" + +#: ../pykolab/cli/cmd_add_alias.py:105 +#, python-format +msgid "Recipient %r is not the primary recipient for address %r" +msgstr "" + +#: ../pykolab/cli/cmd_add_domain.py:36 +#: ../pykolab/cli/cmd_count_domain_mailboxes.py:38 +#: ../pykolab/cli/cmd_create_mailbox.py:36 #: ../pykolab/cli/cmd_export_mailbox.py:33 -#: ../pykolab/cli/cmd_list_mailboxes.py:39 +#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:38 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:36 +#: ../pykolab/cli/cmd_list_mailboxes.py:40 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:37 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:37 +#: ../pykolab/cli/cmd_list_messages.py:37 ../pykolab/cli/cmd_list_quota.py:36 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:36 +#: ../pykolab/cli/cmd_server_info.py:34 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:38 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:39 +#: ../pykolab/cli/cmd_undelete_mailbox.py:34 msgid "CLI Options" -msgstr "" +msgstr "Kommandozeilenoptionen" #: ../pykolab/cli/cmd_add_domain.py:42 -msgid "Add domain as alias for DOMAIN" +msgid "Add alias domain." msgstr "" #: ../pykolab/cli/cmd_add_domain.py:47 -msgid "Add a new domain or domain alias." +msgid "Add a new domain." msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:55 +#: ../pykolab/cli/cmd_add_domain.py:55 ../pykolab/cli/cmd_delete_domain.py:44 +#: ../pykolab/cli/cmd_find_domain.py:44 msgid "Could not find credentials with sufficient permissions" msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:80 ../pykolab/wap_client/__init__.py:113 -msgid "Invalid parent domain" +#: ../pykolab/cli/cmd_add_domain.py:67 ../pykolab/cli/cmd_delete_domain.py:56 +#: ../pykolab/cli/cmd_find_domain.py:56 +msgid "Domain name" msgstr "" -#: ../pykolab/cli/cmd_add_domain.py:86 -msgid "Domain name" +#: ../pykolab/cli/cmd_add_user_subscription.py:37 +msgid "Subscribe a user to a folder." +msgstr "" + +#: ../pykolab/cli/cmd_add_user_subscription.py:47 +#: ../pykolab/cli/cmd_add_user_subscription.py:51 +#: ../pykolab/cli/cmd_remove_user_subscription.py:47 +#: ../pykolab/cli/cmd_remove_user_subscription.py:51 +msgid "Folder pattern" +msgstr "" + +#: ../pykolab/cli/cmd_add_user_subscription.py:50 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:63 +#: ../pykolab/cli/cmd_remove_user_subscription.py:50 +msgid "User ID" +msgstr "" + +#: ../pykolab/cli/cmd_add_user_subscription.py:72 +#: ../pykolab/cli/cmd_remove_user_subscription.py:72 +#, python-format +msgid "Cannot subscribe user to folder %r:" +msgstr "" + +#: ../pykolab/cli/cmd_add_user_subscription.py:73 +#: ../pykolab/cli/cmd_delete_message.py:61 +#: ../pykolab/cli/cmd_list_messages.py:67 +#: ../pykolab/cli/cmd_remove_user_subscription.py:73 +msgid "No such folder" +msgstr "" + +#: ../pykolab/cli/cmd_count_domain_mailboxes.py:44 +#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:50 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:48 +#: ../pykolab/cli/cmd_list_mailboxes.py:52 ../pykolab/cli/cmd_list_quota.py:42 +#: ../pykolab/cli/cmd_server_info.py:40 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:57 +msgid "List mailboxes on server SERVER only." msgstr "" #: ../pykolab/cli/cmd_create_mailbox.py:42 msgid "Set metadata for folder to ANNOTATION=VALUE" msgstr "" -#: ../pykolab/cli/cmd_create_mailbox.py:52 -msgid "Invalid argument" +#: ../pykolab/cli/cmd_create_mailbox.py:50 +msgid "Create folder on PARTITION." msgstr "" #: ../pykolab/cli/cmd_create_mailbox.py:60 +msgid "Invalid argument" +msgstr "" + +#: ../pykolab/cli/cmd_create_mailbox.py:68 msgid "Invalid argument for metadata" msgstr "" +#: ../pykolab/cli/cmd_delete_domain.py:36 +msgid "Delete a domain." +msgstr "" + #: ../pykolab/cli/cmd_delete_mailbox_acl.py:45 #: ../pykolab/cli/cmd_delete_mailbox_acl.py:49 #: ../pykolab/cli/cmd_set_mailbox_acl.py:50 @@ -684,7 +919,11 @@ msgstr "" #: ../pykolab/cli/cmd_list_mailbox_acls.py:43 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:54 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:65 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:66 +#: ../pykolab/cli/cmd_set_quota.py:46 ../tests/unit/test-015-translate.py:12 +#: ../tests/unit/test-015-translate.py:16 +#: ../tests/unit/test-015-translate.py:18 +#: ../tests/unit/test-015-translate.py:20 msgid "Folder name" msgstr "" @@ -692,7 +931,8 @@ msgstr "" #: ../pykolab/cli/cmd_list_mailbox_acls.py:54 #: ../pykolab/cli/cmd_list_mailbox_metadata.py:80 #: ../pykolab/cli/cmd_set_mailbox_acl.py:67 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:93 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:94 +#: ../pykolab/cli/cmd_set_quota.py:58 #, python-format msgid "No such folder %r" msgstr "" @@ -701,6 +941,23 @@ msgstr "" msgid "No mailbox specified" msgstr "" +#: ../pykolab/cli/cmd_delete_mailbox.py:56 +msgid "No such folder(s)" +msgstr "" + +#: ../pykolab/cli/cmd_delete_message.py:36 +msgid "Delete a message from a folder" +msgstr "" + +#: ../pykolab/cli/cmd_delete_message.py:49 +msgid "Specify a UID" +msgstr "" + +#: ../pykolab/cli/cmd_delete_message.py:52 +#: ../pykolab/cli/cmd_list_messages.py:58 +msgid "Specify a folder" +msgstr "" + #: ../pykolab/cli/cmd_export_mailbox.py:38 msgid "All folders this user has access to" msgstr "" @@ -720,11 +977,22 @@ msgstr "" msgid "No directories found for user %s" msgstr "" -#: ../pykolab/cli/cmd_list_mailboxes.py:44 +#: ../pykolab/cli/cmd_find_domain.py:36 +msgid "Find a domain." +msgstr "" + +#: ../pykolab/cli/cmd_list_deleted_mailboxes.py:43 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:41 +#: ../pykolab/cli/cmd_list_mailboxes.py:45 +#: ../pykolab/cli/cmd_list_user_subscriptions.py:41 msgid "Display raw IMAP UTF-7 folder names" msgstr "" -#: ../pykolab/cli/cmd_list_mailboxes.py:75 +#: ../pykolab/cli/cmd_list_domain_mailboxes.py:58 +msgid "Domain" +msgstr "Domäne" + +#: ../pykolab/cli/cmd_list_mailboxes.py:87 #, python-format msgid "Appending folder search for %r" msgstr "" @@ -733,11 +1001,41 @@ msgstr "" msgid "List annotations as user USER" msgstr "" -#: ../pykolab/cli/cmd_list_quota.py:59 ../pykolab/cli/cmd_list_quota.py:71 +#: ../pykolab/cli/cmd_list_messages.py:43 +msgid "Include messages flagged as \\Deleted" +msgstr "" + +#: ../pykolab/cli/cmd_list_messages.py:47 +msgid "List messages in a folder" +msgstr "" + +#: ../pykolab/cli/cmd_list_quota.py:73 ../pykolab/cli/cmd_list_quota.py:89 #, python-format msgid "The quota for folder %s is set to literally allow 0KB of storage." msgstr "" +#: ../pykolab/cli/cmd_list_user_subscriptions.py:47 +msgid "List unsubscribed folders" +msgstr "" + +#: ../pykolab/cli/cmd_list_user_subscriptions.py:50 +msgid "List the folders a user is subscribed to." +msgstr "" + +#: ../pykolab/cli/cmd_list_user_subscriptions.py:98 +#, python-format +msgid "No unsubscribed folders for user %s" +msgstr "" + +#: ../pykolab/cli/cmd_mailbox_cleanup.py:37 +msgid "Clean up mailboxes that do no longer have an owner." +msgstr "" + +#: ../pykolab/cli/cmd_mailbox_cleanup.py:61 +#, python-format +msgid "Deleting folder 'user/%s'" +msgstr "" + #: ../pykolab/cli/cmd_remove_mailaddress.py:49 msgid "Invalid or unqualified email address." msgstr "" @@ -761,20 +1059,34 @@ msgstr "" msgid "Found the following recipients:" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:48 +#: ../pykolab/cli/cmd_remove_user_subscription.py:37 +msgid "Unsubscribe a user from a folder." +msgstr "" + +#: ../pykolab/cli/cmd_remove_user_subscription.py:86 +#, python-format +msgid "Successfully unsubscribed user %s from the following folders:" +msgstr "" + +#: ../pykolab/cli/cmd_remove_user_subscription.py:92 +#, python-format +msgid "User %s was not unsubscribed from any folders." +msgstr "" + +#: ../pykolab/cli/cmd_rename_mailbox.py:52 msgid "No target mailbox name specified" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:50 +#: ../pykolab/cli/cmd_rename_mailbox.py:54 msgid "No source mailbox name specified" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:62 +#: ../pykolab/cli/cmd_rename_mailbox.py:66 #, python-format msgid "Source folder %r does not exist" msgstr "" -#: ../pykolab/cli/cmd_rename_mailbox.py:66 +#: ../pykolab/cli/cmd_rename_mailbox.py:70 #, python-format msgid "Target folder %r already exists" msgstr "" @@ -785,21 +1097,75 @@ msgstr "" msgid "ACI Permissions" msgstr "" -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:44 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:45 msgid "Set annotation as user USER" msgstr "" -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:58 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:62 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:59 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:63 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:68 msgid "Metadata value" msgstr "" -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:61 -#: ../pykolab/cli/cmd_set_mailbox_metadata.py:66 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:62 +#: ../pykolab/cli/cmd_set_mailbox_metadata.py:67 msgid "Metadata path" msgstr "" +#: ../pykolab/cli/cmd_set_quota.py:43 ../pykolab/cli/cmd_set_quota.py:47 +msgid "New quota" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:44 +msgid "Delete mailboxes for recipients that do not appear to exist in LDAP." +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:50 +msgid "Display changes, do not apply them." +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:88 +#, python-format +msgid "Domains in IMAP not in LDAP: %r" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:101 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:142 +#, python-format +msgid "" +"No recipients for '%s' (would have deleted the mailbox if not for --dry-" +"run)!" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:106 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:147 +#, python-format +msgid "Deleting mailbox '%s' because it has no recipients" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:110 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:151 +#, python-format +msgid "An error occurred removing mailbox %r: %r" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:112 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:153 +#, python-format +msgid "Not automatically deleting shared folder '%s'" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:114 +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:155 +#, python-format +msgid "No recipients for '%s' (use --delete to delete)!" +msgstr "" + +#: ../pykolab/cli/cmd_sync_mailhost_attrs.py:136 +#, python-format +msgid "Multiple recipients for '%s'!" +msgstr "" + #: ../pykolab/cli/cmd_sync.py:41 #, python-format msgid "Found %d domains in %d seconds" @@ -810,37 +1176,126 @@ msgstr "" msgid "Running for domain %s" msgstr "" -#: ../pykolab/cli/cmd_sync.py:57 +#: ../pykolab/cli/cmd_sync.py:58 #, python-format msgid "Synchronizing users for %s took %d seconds" msgstr "" +#: ../pykolab/cli/cmd_undelete_mailbox.py:39 +msgid "Do not actually execute, but state what would have been executed." +msgstr "" + +#: ../pykolab/cli/cmd_undelete_mailbox.py:42 +msgid "Recover mailboxes previously deleted." +msgstr "" + +#: ../pykolab/cli/cmd_user_info.py:39 +msgid "Email address" +msgstr "" + #. This is a nested command #. This is a nested component -#: ../pykolab/cli/commands.py:101 ../pykolab/setup/components.py:90 +#: ../pykolab/cli/commands.py:98 ../pykolab/setup/components.py:90 #, python-format msgid "Command Group: %s" msgstr "" -#: ../pykolab/cli/commands.py:116 ../pykolab/cli/commands.py:121 +#: ../pykolab/cli/commands.py:113 ../pykolab/cli/commands.py:118 msgid "No such command." msgstr "" -#: ../pykolab/cli/commands.py:171 ../pykolab/setup/components.py:231 +#: ../pykolab/cli/commands.py:168 ../pykolab/setup/components.py:231 #, python-format msgid "Command '%s' already registered" msgstr "" -#: ../pykolab/cli/commands.py:196 ../pykolab/setup/components.py:257 -#: ../wallace/modules.py:348 +#: ../pykolab/cli/commands.py:193 ../pykolab/setup/components.py:257 +#: ../wallace/modules.py:369 #, python-format msgid "Alias for %s" msgstr "" -#: ../pykolab/cli/commands.py:204 ../pykolab/setup/components.py:265 +#: ../pykolab/cli/commands.py:201 ../pykolab/setup/components.py:265 msgid "Not yet implemented" msgstr "" +#: ../pykolab/cli/sieve/cmd_list.py:43 ../pykolab/cli/sieve/cmd_put.py:42 +#: ../pykolab/cli/sieve/cmd_refresh.py:44 ../pykolab/cli/sieve/cmd_test.py:43 +msgid "Email Address" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:99 +#: ../pykolab/plugins/sievemgmt/__init__.py:111 +#, python-format +msgid "Found the following scripts for user %s: %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:100 +#: ../pykolab/plugins/sievemgmt/__init__.py:112 +#, python-format +msgid "And the following script is active for user %s: %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:178 +#: ../pykolab/plugins/sievemgmt/__init__.py:190 +#, python-format +msgid "" +"Delivery to folder active, but no folder name attribute available for user " +"%r" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:181 +#: ../pykolab/plugins/sievemgmt/__init__.py:193 +msgid "Delivery to folder active, but no folder name attribute configured" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:359 +#, python-format +msgid "MANAGEMENT script for user %s contents: %r" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:364 +#: ../pykolab/plugins/sievemgmt/__init__.py:374 +#, python-format +msgid "Uploading script MANAGEMENT failed for user %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:366 +#: ../pykolab/plugins/sievemgmt/__init__.py:376 +#, python-format +msgid "Uploading script MANAGEMENT for user %s succeeded" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:377 +#: ../pykolab/plugins/sievemgmt/__init__.py:387 +#, python-format +msgid "Including script %s in USER (for user %s)" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:386 +#: ../pykolab/plugins/sievemgmt/__init__.py:396 +#, python-format +msgid "Uploading script USER failed for user %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:388 +#: ../pykolab/plugins/sievemgmt/__init__.py:398 +#, python-format +msgid "Uploading script USER for user %s succeeded" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:416 +#: ../pykolab/plugins/sievemgmt/__init__.py:426 +#, python-format +msgid "Uploading script MASTER failed for user %s" +msgstr "" + +#: ../pykolab/cli/sieve/cmd_refresh.py:418 +#: ../pykolab/plugins/sievemgmt/__init__.py:428 +#, python-format +msgid "Uploading script MASTER for user %s succeeded" +msgstr "" + #: ../pykolab/cli/telemetry/cmd_examine_command_issue.py:40 msgid "Unspecified command issue identifier" msgstr "" @@ -986,357 +1441,429 @@ msgstr "" msgid "No command supplied" msgstr "" -#: ../pykolab/conf/__init__.py:411 +#: ../pykolab/conf/__init__.py:416 msgid "Insufficient options. Need section, key and value -in that order." msgstr "" -#: ../pykolab/conf/__init__.py:414 +#: ../pykolab/conf/__init__.py:419 #, python-format msgid "No section '%s' exists." msgstr "" -#: ../pykolab/conf/__init__.py:445 +#: ../pykolab/conf/__init__.py:461 #, python-format msgid "Setting %s to %r (from the default values for CLI options)" msgstr "" -#: ../pykolab/conf/__init__.py:514 +#: ../pykolab/conf/__init__.py:534 #, python-format msgid "Could not execute configuration function: %s" msgstr "" -#: ../pykolab/conf/__init__.py:522 +#: ../pykolab/conf/__init__.py:542 #, python-format msgid "Option %s/%s does not exist in config file %s, pulling from defaults" msgstr "" -#: ../pykolab/conf/__init__.py:530 ../pykolab/conf/__init__.py:533 +#: ../pykolab/conf/__init__.py:550 ../pykolab/conf/__init__.py:553 msgid "Option does not exist in defaults." msgstr "" -#: ../pykolab/conf/__init__.py:543 +#: ../pykolab/conf/__init__.py:563 #, python-format msgid "Configuration file %s not readable." msgstr "" -#: ../pykolab/conf/__init__.py:546 +#: ../pykolab/conf/__init__.py:566 #, python-format msgid "Configuration file %s does not exist." msgstr "" -#: ../pykolab/conf/__init__.py:551 +#: ../pykolab/conf/__init__.py:571 msgid "" -"WARNING: A negative debug level value does not make this program be any more " -"silent." +"WARNING: A negative debug level value does not make this program be any more" +" silent." msgstr "" -#: ../pykolab/conf/__init__.py:557 +#: ../pykolab/conf/__init__.py:577 msgid "This program has 9 levels of verbosity. Using the maximum of 9." msgstr "" -#: ../pykolab/conf/__init__.py:565 ../pykolab/conf/__init__.py:571 +#: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591 msgid "Cannot start SASL authentication daemon" msgstr "" -#: ../pykolab/conf/__init__.py:582 +#: ../pykolab/conf/__init__.py:602 msgid "No imaplib library found." msgstr "" -#: ../pykolab/conf/__init__.py:592 +#: ../pykolab/conf/__init__.py:612 msgid "No LMTP class found in the smtplib library." msgstr "" -#: ../pykolab/conf/__init__.py:602 +#: ../pykolab/conf/__init__.py:622 msgid "No SMTP class found in the smtplib library." msgstr "" -#: ../pykolab/conf/__init__.py:616 +#: ../pykolab/conf/__init__.py:636 #, python-format msgid "Found you specified a specific set of items to test: %s" msgstr "" -#: ../pykolab/conf/__init__.py:624 +#: ../pykolab/conf/__init__.py:644 #, python-format msgid "Selectively selecting: %s" msgstr "" #: ../pykolab/constants.py.in:40 -msgid "PyKolab is a Kolab Systems product. For more information " +msgid "" +"PyKolab is a Kolab Systems product. For more information about Kolab or " +"PyKolab, visit http://www.kolabsys.com" msgstr "" -#: ../pykolab/constants.py.in:54 +#: ../pykolab/constants.py.in:53 msgid "WARNING" msgstr "" -#: ../pykolab/constants.py.in:54 -msgid "The Fully Qualified " +#: ../pykolab/constants.py.in:53 +msgid "" +"The Fully Qualified Domain Name or FQDN for this system is incorrect. " +"Falling back to 'localdomain'." msgstr "" -#: ../pykolab/constants.py.in:75 +#: ../pykolab/constants.py.in:72 msgid "389 Directory Server or Red Hat Directory Server" msgstr "" -#: ../pykolab/constants.py.in:79 ../pykolab/constants.py.in:83 +#: ../pykolab/constants.py.in:76 ../pykolab/constants.py.in:80 msgid "OpenLDAP or compatible" msgstr "" -#: ../pykolab/imap/cyrus.py:79 +#: ../pykolab/imap/cyrus.py:80 #, python-format msgid "Could not connect to Cyrus IMAP server %r" msgstr "" -#: ../pykolab/imap/cyrus.py:136 +#: ../pykolab/imap/cyrus.py:137 #, python-format msgid "Continuing with separator: %r" msgstr "" -#: ../pykolab/imap/cyrus.py:141 +#: ../pykolab/imap/cyrus.py:142 msgid "Detected we are running in a Murder topology" msgstr "" -#: ../pykolab/imap/cyrus.py:145 +#: ../pykolab/imap/cyrus.py:146 msgid "This system is not part of a murder topology" msgstr "" -#: ../pykolab/imap/cyrus.py:166 +#: ../pykolab/imap/cyrus.py:167 #, python-format msgid "Checking actual backend server for folder %s through annotations" msgstr "" -#: ../pykolab/imap/cyrus.py:181 +#: ../pykolab/imap/cyrus.py:172 +msgid "Possibly reproducing the find " +msgstr "" + +#: ../pykolab/imap/cyrus.py:195 #, python-format msgid "Could not get the annotations after %s tries." msgstr "" -#: ../pykolab/imap/cyrus.py:185 +#: ../pykolab/imap/cyrus.py:199 #, python-format msgid "No annotations for %s: %r" msgstr "" -#: ../pykolab/imap/cyrus.py:192 +#: ../pykolab/imap/cyrus.py:206 #, python-format msgid "Server for INBOX folder %s is %s" msgstr "" -#: ../pykolab/imap/cyrus.py:204 +#: ../pykolab/imap/cyrus.py:226 #, python-format msgid "Setting quota for folder %s to %s" msgstr "" -#: ../pykolab/imap/cyrus.py:208 +#: ../pykolab/imap/cyrus.py:230 #, python-format msgid "Could not set quota for mailfolder %s" msgstr "" -#: ../pykolab/imap/cyrus.py:217 +#: ../pykolab/imap/cyrus.py:239 #, python-format msgid "Moving INBOX folder %s to %s" msgstr "" -#: ../pykolab/imap/cyrus.py:232 +#: ../pykolab/imap/cyrus.py:254 #, python-format msgid "Setting annotation %s on folder %s" msgstr "" -#: ../pykolab/imap/cyrus.py:237 +#: ../pykolab/imap/cyrus.py:259 #, python-format msgid "Could not set annotation %r on mail folder %r: %r" msgstr "" -#: ../pykolab/imap/cyrus.py:241 +#: ../pykolab/imap/cyrus.py:263 #, python-format msgid "Transferring folder %s from %s to %s" msgstr "" -#: ../pykolab/imap/cyrus.py:301 +#: ../pykolab/imap/cyrus.py:323 #, python-format msgid "Undeleting %s to %s" msgstr "" -#: ../pykolab/imap/__init__.py:45 +#: ../pykolab/imap/cyrus.py:334 +#, python-format +msgid "Would have transfered %s from %s to %s" +msgstr "" + +#: ../pykolab/imap/cyrus.py:336 +#, python-format +msgid "Would have renamed %s to %s" +msgstr "" + +#: ../pykolab/imap/__init__.py:46 #, python-format msgid "Cleaning up ACL entries for %s across all folders" msgstr "" -#: ../pykolab/imap/__init__.py:60 +#: ../pykolab/imap/__init__.py:61 #, python-format msgid "Cleaning up ACL entries referring to identifier %s" msgstr "" -#: ../pykolab/imap/__init__.py:69 +#: ../pykolab/imap/__init__.py:70 #, python-format msgid "Iterating over %d folders" msgstr "" #. Set the ACL to '' (effectively deleting the ACL entry) -#: ../pykolab/imap/__init__.py:82 +#: ../pykolab/imap/__init__.py:83 #, python-format msgid "Removing acl %r for subject %r from folder %r" msgstr "" -#: ../pykolab/imap/__init__.py:143 +#: ../pykolab/imap/__init__.py:145 +msgid "No administrator password is available." +msgstr "" + +#: ../pykolab/imap/__init__.py:153 #, python-format msgid "Logging on to Cyrus IMAP server %s" msgstr "" -#: ../pykolab/imap/__init__.py:152 +#: ../pykolab/imap/__init__.py:162 #, python-format msgid "Logging on to Dovecot IMAP server %s" msgstr "" -#: ../pykolab/imap/__init__.py:161 +#: ../pykolab/imap/__init__.py:171 #, python-format msgid "Logging on to generic IMAP server %s" msgstr "" -#: ../pykolab/imap/__init__.py:179 +#: ../pykolab/imap/__init__.py:189 #, python-format msgid "Reusing existing IMAP server connection to %s" msgstr "" -#: ../pykolab/imap/__init__.py:181 +#: ../pykolab/imap/__init__.py:191 #, python-format msgid "Reconnecting to IMAP server %s" msgstr "" -#: ../pykolab/imap/__init__.py:197 +#: ../pykolab/imap/__init__.py:208 msgid "Called imap.disconnect() on a server that we had no connection to." msgstr "" -#: ../pykolab/imap/__init__.py:212 +#: ../pykolab/imap/__init__.py:222 ../pykolab/imap/__init__.py:234 #, python-format -msgid "%r has no attribute %s" +msgid "Could not create folder %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:223 +#, python-format +msgid " on server %r" msgstr "" -#: ../pykolab/imap/__init__.py:279 -msgid "Private annotations need to be set using the appropriate user account." +#: ../pykolab/imap/__init__.py:244 ../pykolab/imap/__init__.py:246 +#, python-format +msgid "%r has no attribute %s" msgstr "" -#: ../pykolab/imap/__init__.py:294 ../pykolab/imap/__init__.py:329 +#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428 #, python-format msgid "Creating new shared folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:354 ../pykolab/imap/__init__.py:504 +#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675 #, python-format msgid "Downcasing mailbox name %r" msgstr "" -#: ../pykolab/imap/__init__.py:358 +#: ../pykolab/imap/__init__.py:457 #, python-format msgid "Creating new mailbox for user %s" msgstr "" -#: ../pykolab/imap/__init__.py:387 +#: ../pykolab/imap/__init__.py:470 +msgid "Waiting for the Cyrus IMAP Murder to settle..." +msgstr "" + +#: ../pykolab/imap/__init__.py:516 #, python-format msgid "Creating additional folders for user %s" msgstr "" -#: ../pykolab/imap/__init__.py:407 +#: ../pykolab/imap/__init__.py:535 +#, python-format +msgid "Waiting for the Cyrus murder to settle... %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:547 +#, python-format +msgid "Correcting additional folder name from %r to %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:553 #, python-format msgid "Mailbox already exists: %s" msgstr "" -#: ../pykolab/imap/__init__.py:443 +#: ../pykolab/imap/__init__.py:593 msgid "Subscribing user to the additional folders" msgstr "" -#: ../pykolab/imap/__init__.py:458 +#: ../pykolab/imap/__init__.py:607 +msgid "Using the following tests for folder subscriptions:" +msgstr "" + +#: ../pykolab/imap/__init__.py:609 +#, python-format +msgid " %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:612 #, python-format msgid "Folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:470 +#: ../pykolab/imap/__init__.py:624 #, python-format msgid "Subscribing %s to folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:520 ../pykolab/imap/__init__.py:594 +#: ../pykolab/imap/__init__.py:628 +#, python-format +msgid "Subscribing %s to folder %s failed: %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:658 +#, python-format +msgid "Could not rename %s to reside on partition %s" +msgstr "" + +#: ../pykolab/imap/__init__.py:691 +#, python-format +msgid "INBOX folder to rename (%s) does not exist" +msgstr "" + +#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770 #, python-format msgid "Renaming INBOX from %s to %s" msgstr "" -#: ../pykolab/imap/__init__.py:524 +#: ../pykolab/imap/__init__.py:698 #, python-format msgid "Could not rename INBOX folder %s to %s" msgstr "" -#: ../pykolab/imap/__init__.py:526 ../pykolab/imap/__init__.py:598 +#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774 #, python-format -msgid "Moving INBOX folder %s won't succeed as target folder %s already exists" +msgid "" +"Moving INBOX folder %s won't succeed as target folder %s already exists" msgstr "" -#: ../pykolab/imap/__init__.py:536 +#: ../pykolab/imap/__init__.py:704 +#, python-format +msgid "Server for mailbox %r is %r" +msgstr "" + +#: ../pykolab/imap/__init__.py:712 #, python-format msgid "Looking for folder '%s', we found folders: %r" msgstr "" -#: ../pykolab/imap/__init__.py:559 +#: ../pykolab/imap/__init__.py:735 #, python-format msgid "Setting ACL rights %s for subject %s on folder " msgstr "" -#: ../pykolab/imap/__init__.py:570 +#: ../pykolab/imap/__init__.py:746 #, python-format msgid "Removing ACL rights %s for subject %s on folder " msgstr "" -#: ../pykolab/imap/__init__.py:591 +#: ../pykolab/imap/__init__.py:767 #, python-format msgid "Found old INBOX folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:600 +#: ../pykolab/imap/__init__.py:776 #, python-format msgid "Did not find old folder user/%s to rename" msgstr "" -#: ../pykolab/imap/__init__.py:602 +#: ../pykolab/imap/__init__.py:778 msgid "Value for user is not a dictionary" msgstr "" #. TODO: Go in fact correct the quota. -#: ../pykolab/imap/__init__.py:662 +#: ../pykolab/imap/__init__.py:846 #, python-format msgid "Cannot get current IMAP quota for folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:675 +#: ../pykolab/imap/__init__.py:859 #, python-format msgid "Quota for %s currently is %s" msgstr "" -#: ../pykolab/imap/__init__.py:681 +#: ../pykolab/imap/__init__.py:865 #, python-format msgid "Adjusting authentication database quota for folder %s to %d" msgstr "" -#: ../pykolab/imap/__init__.py:686 +#: ../pykolab/imap/__init__.py:870 #, python-format msgid "Correcting quota for %s to %s (currently %s)" msgstr "" -#: ../pykolab/imap/__init__.py:763 +#: ../pykolab/imap/__init__.py:947 #, python-format msgid "Checking folder: %s" msgstr "" -#: ../pykolab/imap/__init__.py:768 +#: ../pykolab/imap/__init__.py:952 #, python-format msgid "Folder has no corresponding user (1): %s" msgstr "" -#: ../pykolab/imap/__init__.py:771 +#: ../pykolab/imap/__init__.py:955 #, python-format msgid "Folder has no corresponding user (2): %s" msgstr "" #. We got user identifier only -#: ../pykolab/imap/__init__.py:786 +#: ../pykolab/imap/__init__.py:970 msgid "Please don't give us just a user identifier" msgstr "" -#: ../pykolab/imap/__init__.py:789 +#: ../pykolab/imap/__init__.py:973 #, python-format msgid "Deleting folder %s" msgstr "" @@ -1345,12 +1872,50 @@ msgstr "" msgid "Returning thread local configuration" msgstr "" -#: ../pykolab/logger.py:106 +#: ../pykolab/itip/__init__.py:43 +#, python-format +msgid "Method %r not really interesting for us." +msgstr "" + +#: ../pykolab/itip/__init__.py:49 +#, python-format +msgid "Raw iTip payload: %s" +msgstr "" + +#: ../pykolab/itip/__init__.py:59 +msgid "Could not read iTip from message." +msgstr "" + +#: ../pykolab/itip/__init__.py:67 +#, python-format +msgid "Duplicate iTip object: %s" +msgstr "" + +#: ../pykolab/itip/__init__.py:90 +msgid "iTip event without a start" +msgstr "" + +#: ../pykolab/itip/__init__.py:132 +msgid "Message is not an iTip message (non-multipart message)" +msgstr "" + +#: ../pykolab/itip/__init__.py:225 +#, python-format +msgid "Failed to compose iTip reply message: %r" +msgstr "" + +#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936 +#: ../wallace/module_resources.py:964 +#, python-format +msgid "SMTP sendmail error: %r" +msgstr "" + +#: ../pykolab/logger.py:173 ../pykolab/logger.py:179 #, python-format -msgid "Could not change the ownership of log file %s" +msgid "Could not change permissions on %s: %r" msgstr "" -#: ../pykolab/logger.py:122 +#: ../pykolab/logger.py:196 #, python-format msgid "Cannot log to file %s: %s" msgstr "" @@ -1448,27 +2013,51 @@ msgstr "" msgid "Attribute substitution for 'mail' failed in Recipient Policy" msgstr "" -#: ../pykolab/plugins/recipientpolicy/__init__.py:115 +#: ../pykolab/plugins/recipientpolicy/__init__.py:116 msgid "Could not parse the alternative mail routines" msgstr "" -#: ../pykolab/plugins/recipientpolicy/__init__.py:119 +#: ../pykolab/plugins/recipientpolicy/__init__.py:120 #, python-format msgid "Alternative mail routines: %r" msgstr "" -#: ../pykolab/plugins/recipientpolicy/__init__.py:130 -#: ../pykolab/plugins/recipientpolicy/__init__.py:141 +#: ../pykolab/plugins/recipientpolicy/__init__.py:127 +#, python-format +msgid "" +"An error occurred in composing the secondary mail attribute for entry %r" +msgstr "" + +#: ../pykolab/plugins/recipientpolicy/__init__.py:138 +#: ../pykolab/plugins/recipientpolicy/__init__.py:153 #, python-format msgid "Appending additional mail address: %s" msgstr "" -#: ../pykolab/plugins/recipientpolicy/__init__.py:134 -#: ../pykolab/plugins/recipientpolicy/__init__.py:145 +#: ../pykolab/plugins/recipientpolicy/__init__.py:142 +#, python-format +msgid "Policy for secondary email address failed: %r" +msgstr "" + +#: ../pykolab/plugins/recipientpolicy/__init__.py:157 msgid "" "Attribute substitution for 'alternative_mail' failed in Recipient Policy" msgstr "" +#: ../pykolab/plugins/roundcubedb/__init__.py:48 +#, python-format +msgid "user_delete: %r" +msgstr "" + +#: ../pykolab/plugins/roundcubedb/__init__.py:55 +#: ../pykolab/setup/setup_roundcube.py:160 +msgid "Roundcube installation path not found." +msgstr "" + +#: ../pykolab/plugins/sievemgmt/__init__.py:51 +msgid "Wrong number of arguments for sieve management plugin" +msgstr "" + #: ../pykolab/setup/components.py:58 msgid "Display this help." msgstr "" @@ -1489,61 +2078,29 @@ msgstr "" msgid "Free/Busy is not installed on this system" msgstr "" -#: ../pykolab/setup/setup_freebusy.py:55 -msgid "" -"\n" -" Please supply the MySQL password for the " -"'roundcube'\n" -" user. You have supplied this password earlier, and " -"it is\n" -" available from the database URI setting in\n" -" /etc/roundcubemail/db.inc.php.\n" -" " -msgstr "" - -#: ../pykolab/setup/setup_freebusy.py:64 -#: ../pykolab/setup/setup_roundcube.py:56 -msgid "MySQL roundcube password" +#: ../pykolab/setup/setup_imap.py:45 +msgid "Setup IMAP." msgstr "" -#: ../pykolab/setup/setup_freebusy.py:92 -#: ../pykolab/setup/setup_roundcube.py:115 ../pykolab/setup/setup_zpush.py:71 -#, python-format -msgid "Using template file %r" +#: ../pykolab/setup/setup_imap.py:89 +msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf" msgstr "" -#: ../pykolab/setup/setup_freebusy.py:99 -#: ../pykolab/setup/setup_roundcube.py:122 ../pykolab/setup/setup_zpush.py:78 -#, python-format -msgid "Successfully compiled template %r, writing out to %r" +#: ../pykolab/setup/setup_imap.py:114 +msgid "Could not write out Cyrus IMAP configuration file /etc/cyrus.conf" msgstr "" -#: ../pykolab/setup/setup_freebusy.py:119 -#: ../pykolab/setup/setup_roundcube.py:193 -#: ../pykolab/setup/setup_syncroton.py:66 ../pykolab/setup/setup_zpush.py:98 -msgid "Could not start the webserver server service." +#: ../pykolab/setup/setup_imap.py:158 +msgid "Could not start the cyrus-imapd and kolab-saslauthd services." msgstr "" -#: ../pykolab/setup/setup_freebusy.py:128 ../pykolab/setup/setup_imap.py:169 -#: ../pykolab/setup/setup_kolabd.py:81 ../pykolab/setup/setup_ldap.py:327 -#: ../pykolab/setup/setup_mta.py:378 ../pykolab/setup/setup_mysql.py:58 -#: ../pykolab/setup/setup_roundcube.py:202 -#: ../pykolab/setup/setup_syncroton.py:75 ../pykolab/setup/setup_zpush.py:107 +#: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:81 +#: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:455 +#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:237 +#: ../pykolab/setup/setup_syncroton.py:102 msgid "Could not configure to start on boot, the " msgstr "" -#: ../pykolab/setup/setup_imap.py:45 -msgid "Setup IMAP." -msgstr "" - -#: ../pykolab/setup/setup_imap.py:89 ../pykolab/setup/setup_imap.py:114 -msgid "Could not write out Cyrus IMAP configuration file /etc/imapd.conf" -msgstr "" - -#: ../pykolab/setup/setup_imap.py:154 -msgid "Could not start the cyrus-imapd and kolab-saslauthd services." -msgstr "" - #: ../pykolab/setup/setup_kolabd.py:43 msgid "Setup the Kolab daemon." msgstr "" @@ -1552,8 +2109,7 @@ msgstr "" #, python-format msgid "" "\n" -" Copying the configuration section for 'example." -"org' over to\n" +" Copying the configuration section for 'example.org' over to\n" " a section applicable to your domain '%s'.\n" " " msgstr "" @@ -1574,220 +2130,250 @@ msgstr "" msgid "Allow anonymous binds (default: no)." msgstr "" -#: ../pykolab/setup/setup_ldap.py:64 -msgid "Setup LDAP." +#: ../pykolab/setup/setup_ldap.py:68 +msgid "Skip setting up the LDAP server." msgstr "" #: ../pykolab/setup/setup_ldap.py:76 +msgid "Setup configuration for OpenLDAP compatibility." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:84 +msgid "Setup configuration for Active Directory compatibility." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:88 +msgid "Setup LDAP." +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:97 +msgid "Skipping setup of LDAP, as specified" +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:126 +msgid "" +"\n" +" You can not configure Kolab to run against OpenLDAP\n" +" and Active Directory simultaneously.\n" +" " +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:139 msgid "" "\n" -" Please supply a password for the LDAP administrator " -"user\n" -" 'admin', used to login to the graphical console of " -"389\n" +" It seems 389 Directory Server has an existing\n" +" instance configured. This setup script does not\n" +" intend to destroy or overwrite your data. Please\n" +" make sure /etc/dirsrv/ and /var/lib/dirsrv/ are\n" +" clean so that this setup does not have to worry.\n" +" " +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:154 +msgid "" +"\n" +" Please supply a password for the LDAP administrator user\n" +" 'admin', used to login to the graphical console of 389\n" " Directory server.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:84 +#: ../pykolab/setup/setup_ldap.py:162 msgid "Administrator password" msgstr "" -#: ../pykolab/setup/setup_ldap.py:91 +#: ../pykolab/setup/setup_ldap.py:169 msgid "" "\n" -" Please supply a password for the LDAP Directory " -"Manager\n" -" user, which is the administrator user you will be " -"using\n" -" to at least initially log in to the Web Admin, and " -"that\n" +" Please supply a password for the LDAP Directory Manager\n" +" user, which is the administrator user you will be using\n" +" to at least initially log in to the Web Admin, and that\n" " Kolab uses to perform administrative tasks.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:100 +#: ../pykolab/setup/setup_ldap.py:178 msgid "Directory Manager password" msgstr "" -#: ../pykolab/setup/setup_ldap.py:107 +#: ../pykolab/setup/setup_ldap.py:185 msgid "" "\n" " Please choose the system user and group the service\n" " should use to run under. These should be existing,\n" -" unprivileged, local system POSIX accounts with no " -"shell.\n" +" unprivileged, local system POSIX accounts with no shell.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:117 ../pykolab/setup/setup_ldap.py:120 +#: ../pykolab/setup/setup_ldap.py:195 ../pykolab/setup/setup_ldap.py:198 msgid "User" msgstr "" -#: ../pykolab/setup/setup_ldap.py:118 ../pykolab/setup/setup_ldap.py:121 +#: ../pykolab/setup/setup_ldap.py:196 ../pykolab/setup/setup_ldap.py:199 msgid "Group" msgstr "" -#: ../pykolab/setup/setup_ldap.py:157 +#: ../pykolab/setup/setup_ldap.py:234 msgid "" "\n" -" This setup procedure plans to set up Kolab Groupware " -"for\n" -" the following domain name space. This domain name " -"is\n" +" This setup procedure plans to set up Kolab Groupware for\n" +" the following domain name space. This domain name is\n" " obtained from the reverse DNS entry on your network\n" -" interface. Please confirm this is the appropriate " -"domain\n" +" interface. Please confirm this is the appropriate domain\n" " name space.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:171 +#: ../pykolab/setup/setup_ldap.py:248 msgid "Domain name to use" msgstr "" -#: ../pykolab/setup/setup_ldap.py:176 ../pykolab/setup/setup_ldap.py:201 +#: ../pykolab/setup/setup_ldap.py:253 ../pykolab/setup/setup_ldap.py:278 msgid "" "\n" " Invalid input. Please try again.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:185 +#: ../pykolab/setup/setup_ldap.py:262 msgid "" "\n" -" The standard root dn we composed for you follows. " -"Please\n" +" The standard root dn we composed for you follows. Please\n" " confirm this is the root dn you wish to use.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:196 +#: ../pykolab/setup/setup_ldap.py:273 msgid "Root DN to use" msgstr "" -#: ../pykolab/setup/setup_ldap.py:244 +#: ../pykolab/setup/setup_ldap.py:325 msgid "No directory server setup tool available." msgstr "" -#: ../pykolab/setup/setup_ldap.py:255 +#: ../pykolab/setup/setup_ldap.py:337 msgid "" "\n" -" Setup is now going to set up the 389 Directory Server. " -"This\n" -" may take a little while (during which period there is " -"no\n" +" Setup is now going to set up the 389 Directory Server. This\n" +" may take a little while (during which period there is no\n" " output and no progress indication).\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:262 +#: ../pykolab/setup/setup_ldap.py:344 msgid "Setting up 389 Directory Server" msgstr "" -#. TODO: Get the return code and display output if not successful. -#: ../pykolab/setup/setup_ldap.py:274 +#: ../pykolab/setup/setup_ldap.py:356 +msgid "" +"\n" +" An error was detected in the setup procedure for 389\n" +" Directory Server. This setup will write out stderr and\n" +" stdout to /var/log/kolab/setup.error.log and\n" +" /var/log/kolab/setup.out.log respectively, before it\n" +" exits.\n" +" " +msgstr "" + +#: ../pykolab/setup/setup_ldap.py:373 msgid "Setup DS stdout:" msgstr "" -#: ../pykolab/setup/setup_ldap.py:277 +#: ../pykolab/setup/setup_ldap.py:376 msgid "Setup DS stderr:" msgstr "" -#: ../pykolab/setup/setup_ldap.py:303 +#: ../pykolab/setup/setup_ldap.py:402 msgid "Could not copy the LDAP extensions for Kolab" msgstr "" -#: ../pykolab/setup/setup_ldap.py:306 +#: ../pykolab/setup/setup_ldap.py:405 msgid "Could not find the ldap Kolab schema file" msgstr "" -#: ../pykolab/setup/setup_ldap.py:318 +#: ../pykolab/setup/setup_ldap.py:417 msgid "Could not start the directory server service." msgstr "" -#: ../pykolab/setup/setup_ldap.py:332 +#: ../pykolab/setup/setup_ldap.py:431 msgid "" "\n" " Please supply a Cyrus Administrator password. This\n" " password is used by Kolab to execute administrative\n" " tasks in Cyrus IMAP. You may also need the password\n" " yourself to troubleshoot Cyrus IMAP and/or perform\n" -" other administrative tasks against Cyrus IMAP " -"directly.\n" +" other administrative tasks against Cyrus IMAP directly.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:342 +#: ../pykolab/setup/setup_ldap.py:441 msgid "Cyrus Administrator password" msgstr "" -#: ../pykolab/setup/setup_ldap.py:349 +#: ../pykolab/setup/setup_ldap.py:448 msgid "" "\n" -" Please supply a Kolab Service account password. " -"This\n" -" account is used by various services such as " -"Postfix,\n" -" and Roundcube, as anonymous binds to the LDAP " -"server\n" +" Please supply a Kolab Service account password. This\n" +" account is used by various services such as Postfix,\n" +" and Roundcube, as anonymous binds to the LDAP server\n" " will not be allowed.\n" " " msgstr "" -#: ../pykolab/setup/setup_ldap.py:358 +#: ../pykolab/setup/setup_ldap.py:457 msgid "Kolab Service password" msgstr "" -#: ../pykolab/setup/setup_ldap.py:368 +#: ../pykolab/setup/setup_ldap.py:467 msgid "Writing out configuration to kolab.conf" msgstr "" -#: ../pykolab/setup/setup_ldap.py:382 +#: ../pykolab/setup/setup_ldap.py:481 msgid "Inserting service users into LDAP." msgstr "" -#: ../pykolab/setup/setup_ldap.py:456 +#: ../pykolab/setup/setup_ldap.py:555 msgid "Writing out cn=kolab,cn=config" msgstr "" #. TODO: Add kolab-admin role #. TODO: Assign kolab-admin admin ACLs -#: ../pykolab/setup/setup_ldap.py:480 +#: ../pykolab/setup/setup_ldap.py:579 #, python-format msgid "Adding domain %s to list of domains for this deployment" msgstr "" -#: ../pykolab/setup/setup_ldap.py:497 +#: ../pykolab/setup/setup_ldap.py:607 msgid "Disabling anonymous binds" msgstr "" #. TODO: Ensure the uid attribute is unique -#. TODO^2: Consider renaming the general "attribute uniqueness to "uid attribute uniqueness" -#: ../pykolab/setup/setup_ldap.py:505 +#. TODO^2: Consider renaming the general "attribute uniqueness to "uid +#. attribute uniqueness" +#: ../pykolab/setup/setup_ldap.py:615 msgid "Enabling attribute uniqueness plugin" msgstr "" -#: ../pykolab/setup/setup_ldap.py:511 +#: ../pykolab/setup/setup_ldap.py:621 msgid "Enabling referential integrity plugin" msgstr "" -#: ../pykolab/setup/setup_ldap.py:517 +#: ../pykolab/setup/setup_ldap.py:627 msgid "Enabling and configuring account policy plugin" msgstr "" #. TODO: Add kolab-admin role -#: ../pykolab/setup/setup_ldap.py:532 +#: ../pykolab/setup/setup_ldap.py:642 msgid "Adding the kolab-admin role" msgstr "" #. TODO: User writeable attributes on root_dn -#: ../pykolab/setup/setup_ldap.py:543 +#: ../pykolab/setup/setup_ldap.py:653 #, python-format msgid "Setting access control to %s" msgstr "" -#: ../pykolab/setup/setup_ldap.py:568 +#: ../pykolab/setup/setup_ldap.py:679 msgid "Could not start and configure to start on boot, the " msgstr "" @@ -1795,24 +2381,24 @@ msgstr "" msgid "Setup MTA." msgstr "" -#: ../pykolab/setup/setup_mta.py:245 ../pykolab/setup/setup_php.py:104 +#: ../pykolab/setup/setup_mta.py:317 ../pykolab/setup/setup_php.py:106 #, python-format msgid "Setting key %r to %r" msgstr "" -#: ../pykolab/setup/setup_mta.py:278 +#: ../pykolab/setup/setup_mta.py:350 msgid "Could not write out Postfix configuration file /etc/postfix/master.cf" msgstr "" -#: ../pykolab/setup/setup_mta.py:321 -msgid "Could not write out Amavis configuration file /etc/amavisd/amavisd.conf" +#: ../pykolab/setup/setup_mta.py:397 +msgid "Could not write out Amavis configuration file amavisd.conf" msgstr "" -#: ../pykolab/setup/setup_mta.py:329 +#: ../pykolab/setup/setup_mta.py:405 msgid "Not writing out any configuration for Amavis." msgstr "" -#: ../pykolab/setup/setup_mta.py:360 +#: ../pykolab/setup/setup_mta.py:437 msgid "Could not start the postfix, clamav and amavisd services services." msgstr "" @@ -1824,55 +2410,50 @@ msgstr "" msgid "Could not start the MySQL database service." msgstr "" -#: ../pykolab/setup/setup_mysql.py:68 +#: ../pykolab/setup/setup_mysql.py:71 msgid "What MySQL server are we setting up?" msgstr "" -#: ../pykolab/setup/setup_mysql.py:72 +#: ../pykolab/setup/setup_mysql.py:75 msgid "" "\n" -" Please supply the root password for MySQL, so we can " -"set\n" -" up user accounts for other components that use " -"MySQL.\n" +" Please supply the root password for MySQL, so we can set\n" +" up user accounts for other components that use MySQL.\n" " " msgstr "" -#: ../pykolab/setup/setup_mysql.py:79 ../pykolab/setup/setup_mysql.py:96 +#: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99 +#: ../pykolab/setup/setup_roundcube.py:183 +#: ../pykolab/setup/setup_syncroton.py:63 msgid "MySQL root password" msgstr "" -#: ../pykolab/setup/setup_mysql.py:85 +#: ../pykolab/setup/setup_mysql.py:88 msgid "" "\n" -" Please supply a root password for MySQL. This " -"password\n" -" will be the administrative user for this MySQL " -"server,\n" -" and it should be kept a secret. After this setup " -"process\n" +" Please supply a root password for MySQL. This password\n" +" will be the administrative user for this MySQL server,\n" +" and it should be kept a secret. After this setup process\n" " has completed, Kolab is going to discard and forget\n" " about this password, but you will need it for\n" " administrative tasks in MySQL.\n" " " msgstr "" -#: ../pykolab/setup/setup_mysql.py:136 +#: ../pykolab/setup/setup_mysql.py:139 msgid "" "\n" -" Please supply a password for the MySQL user " -"'kolab'.\n" -" This password will be used by Kolab services, such " -"as\n" +" Please supply a password for the MySQL user 'kolab'.\n" +" This password will be used by Kolab services, such as\n" " the Web Administration Panel.\n" " " msgstr "" -#: ../pykolab/setup/setup_mysql.py:144 +#: ../pykolab/setup/setup_mysql.py:147 msgid "MySQL kolab password" msgstr "" -#: ../pykolab/setup/setup_mysql.py:162 +#: ../pykolab/setup/setup_mysql.py:165 msgid "Could not find the MySQL Kolab schema file" msgstr "" @@ -1896,19 +2477,21 @@ msgstr "" msgid "" "\n" " Please supply the timezone PHP should be using.\n" +" You have to use a Continent or Country / City locality name\n" +" like 'Europe/Berlin', but not just 'CEST'.\n" " " msgstr "" -#: ../pykolab/setup/setup_php.py:72 +#: ../pykolab/setup/setup_php.py:74 msgid "Timezone ID" msgstr "" -#: ../pykolab/setup/setup_php.py:78 +#: ../pykolab/setup/setup_php.py:80 #, python-format msgid "Cannot configure PHP through %r (No such file or directory)" msgstr "" -#: ../pykolab/setup/setup_php.py:89 +#: ../pykolab/setup/setup_php.py:91 msgid "Could not find PHP configuration file php.ini" msgstr "" @@ -1919,23 +2502,33 @@ msgstr "" #: ../pykolab/setup/setup_roundcube.py:48 msgid "" "\n" -" Please supply a password for the MySQL user " -"'roundcube'.\n" +" Please supply a password for the MySQL user 'roundcube'.\n" " This password will be used by the Roundcube webmail\n" " interface.\n" " " msgstr "" -#: ../pykolab/setup/setup_syncroton.py:40 -msgid "Setup Syncroton." +#: ../pykolab/setup/setup_roundcube.py:56 +msgid "MySQL roundcube password" +msgstr "" + +#: ../pykolab/setup/setup_roundcube.py:120 +#, python-format +msgid "Using template file %r" msgstr "" -#: ../pykolab/setup/setup_zpush.py:41 -msgid "Setup zpush." +#: ../pykolab/setup/setup_roundcube.py:127 +#, python-format +msgid "Successfully compiled template %r, writing out to %r" +msgstr "" + +#: ../pykolab/setup/setup_roundcube.py:228 +#: ../pykolab/setup/setup_syncroton.py:93 +msgid "Could not start the webserver server service." msgstr "" -#: ../pykolab/setup/setup_zpush.py:45 -msgid "Z-Push is not installed on this system" +#: ../pykolab/setup/setup_syncroton.py:40 +msgid "Setup Syncroton." msgstr "" #. start_max = (int)(time.time()) @@ -1963,198 +2556,616 @@ msgstr "" msgid "No database available" msgstr "" -#: ../pykolab/utils.py:60 ../pykolab/utils.py:62 +#: ../pykolab/utils.py:62 ../pykolab/utils.py:64 #, python-format msgid "Confirm %s: " msgstr "" -#: ../pykolab/utils.py:65 +#: ../pykolab/utils.py:67 msgid "Incorrect confirmation. " msgstr "" -#: ../pykolab/utils.py:70 ../pykolab/utils.py:75 +#: ../pykolab/utils.py:72 ../pykolab/utils.py:77 #, python-format msgid "%s: " msgstr "" -#: ../pykolab/utils.py:72 ../pykolab/utils.py:77 +#: ../pykolab/utils.py:74 ../pykolab/utils.py:79 #, python-format msgid "%s [%s]: " msgstr "" -#: ../pykolab/utils.py:122 +#: ../pykolab/utils.py:124 msgid "Please answer 'yes' or 'no'." msgstr "" -#: ../pykolab/utils.py:148 +#: ../pykolab/utils.py:164 msgid "Choice" msgstr "" -#: ../pykolab/utils.py:214 +#: ../pykolab/utils.py:167 +msgid "Choice (type '?' for options)" +msgstr "" + +#: ../pykolab/utils.py:268 #, python-format msgid "Could not change the permissions on %s" msgstr "" -#: ../pykolab/utils.py:395 +#: ../pykolab/utils.py:479 #, python-format msgid "Transliterating string %r with locale %r" msgstr "" -#: ../pykolab/utils.py:403 +#: ../pykolab/utils.py:487 msgid "Attempting to set locale" msgstr "" -#: ../pykolab/utils.py:405 +#: ../pykolab/utils.py:489 msgid "Success setting locale" msgstr "" -#: ../pykolab/utils.py:407 +#: ../pykolab/utils.py:491 msgid "Failure to set locale" msgstr "" -#: ../pykolab/utils.py:415 +#: ../pykolab/utils.py:499 #, python-format msgid "Executing '%s | %s'" msgstr "" -#: ../pykolab/wap_client/__init__.py:257 +#: ../pykolab/utils.py:510 +#, python-format +msgid "Could not translate %s using locale %s" +msgstr "" + +#: ../pykolab/wap_client/__init__.py:320 #, python-format msgid "Requesting %r with params %r" msgstr "" -#: ../pykolab/wap_client/__init__.py:263 +#: ../pykolab/wap_client/__init__.py:328 #, python-format msgid "Got response: %r" msgstr "" #. Some data is not JSON -#: ../pykolab/wap_client/__init__.py:268 +#: ../pykolab/wap_client/__init__.py:334 msgid "Response data is not JSON" msgstr "" -#: ../pykolab/xml/attendee.py:79 ../pykolab/xml/attendee.py:99 +#. support integer values, too +#: ../pykolab/xml/attendee.py:9 ../pykolab/xml/attendee.py:17 +msgid "Needs Action" +msgstr "" + +#: ../pykolab/xml/attendee.py:10 ../pykolab/xml/attendee.py:18 +msgid "Accepted" +msgstr "Akzeptiert" + +#: ../pykolab/xml/attendee.py:11 ../pykolab/xml/attendee.py:19 +msgid "Declined" +msgstr "Abgelehnt" + +#: ../pykolab/xml/attendee.py:12 ../pykolab/xml/attendee.py:20 +msgid "Tentatively Accepted" +msgstr "Provisorisch Akzeptiert" + +#: ../pykolab/xml/attendee.py:13 ../pykolab/xml/attendee.py:21 +msgid "Delegated" +msgstr "Delegiert" + +#: ../pykolab/xml/attendee.py:14 +msgid "Completed" +msgstr "" + +#: ../pykolab/xml/attendee.py:15 +msgid "In Process" +msgstr "" + +#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130 msgid "Not a valid attendee" msgstr "" -#: ../pykolab/xml/attendee.py:84 +#: ../pykolab/xml/attendee.py:115 msgid "No valid delegator references found" msgstr "" -#: ../pykolab/xml/attendee.py:104 +#: ../pykolab/xml/attendee.py:135 msgid "No valid delegatee references found" msgstr "" -#: ../pykolab/xml/attendee.py:140 +#: ../pykolab/xml/attendee.py:180 #, python-format msgid "Invalid cutype %r" msgstr "" -#: ../pykolab/xml/attendee.py:151 +#: ../pykolab/xml/attendee.py:192 #, python-format msgid "Invalid participant status %r" msgstr "" -#: ../pykolab/xml/attendee.py:159 +#: ../pykolab/xml/attendee.py:200 #, python-format msgid "Invalid role %r" msgstr "" -#: ../pykolab/xml/event.py:172 +#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708 +#: ../pykolab/xml/event.py:751 +msgid "Event start needs datetime.date or datetime.datetime instance" +msgstr "" + +#: ../pykolab/xml/event.py:241 #, python-format msgid "No attendee with email or name %r" msgstr "" -#: ../pykolab/xml/event.py:180 +#: ../pykolab/xml/event.py:249 #, python-format msgid "Invalid argument value attendee %r, must be basestring or Attendee" msgstr "" -#: ../pykolab/xml/event.py:186 +#: ../pykolab/xml/event.py:255 #, python-format msgid "No attendee with email %r" msgstr "" -#: ../pykolab/xml/event.py:192 +#: ../pykolab/xml/event.py:261 #, python-format msgid "No attendee with name %r" msgstr "" -#: ../pykolab/xml/event.py:338 +#: ../pykolab/xml/event.py:426 msgid "Invalid participant status" msgstr "" -#: ../pykolab/xml/event.py:538 -msgid "Event end needs datetime.date or datetime.datetime instance" +#: ../pykolab/xml/event.py:542 +#, python-format +msgid "Invalid status %r" msgstr "" -#: ../pykolab/xml/event.py:657 +#: ../pykolab/xml/event.py:550 #, python-format -msgid "Invalid status %r" +msgid "Invalid classification %r" msgstr "" -#: ../pykolab/xml/event.py:678 ../pykolab/xml/event.py:728 -msgid "Event start needs datetime.date or datetime.datetime instance" +#: ../pykolab/xml/event.py:577 +msgid "Event end needs datetime.date or datetime.datetime instance" msgstr "" -#: ../pykolab/xml/event.py:764 +#: ../pykolab/xml/event.py:761 #, python-format msgid "Invalid status set: %r" msgstr "" -#: ../pykolab/xml/event.py:882 +#: ../pykolab/xml/event.py:923 msgid "No sender specified" msgstr "" -#: ../saslauthd/__init__.py:126 ../saslauthd/__init__.py:134 -msgid "Traceback occurred, please report a bug at http://bugzilla.kolabsys.com" +#: ../pykolab/xml/event.py:932 +#, python-format +msgid "Invitation for %s was %s" +msgstr "" + +#: ../pykolab/xml/event.py:937 +msgid "This is an automated response to one of your event requests." +msgstr "" + +#: ../saslauthd/__init__.py:99 +#, python-format +msgid "Could not create %r: %r" +msgstr "" + +#: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145 +#: ../wallace/__init__.py:403 ../wallace/__init__.py:412 +msgid "" +"Traceback occurred, please report a bug at http://bugzilla.kolabsys.com" +msgstr "" + +#: ../saslauthd/__init__.py:185 +msgid "kolab-saslauthd could not accept " +msgstr "" + +#: ../saslauthd/__init__.py:190 +msgid "Maximum tries exceeded, exiting" +msgstr "" + +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190 +#: ../wallace/module_resources.py:879 +#, python-format +msgid "Reservation Request for %(summary)s was %(status)s" +msgstr "" + +#. check notification message sent to resource owner (jane) +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:605 +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:621 +#: ../wallace/module_resources.py:954 +#, python-format +msgid "Booking for %s has been %s" +msgstr "" + +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:146 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:720 +#: ../wallace/module_invitationpolicy.py:374 +#, python-format +msgid "\"%(summary)s\" has been %(status)s" msgstr "" -#: ../wallace/__init__.py:62 +#. check for notification message +#. this notification should be suppressed until mark has replied, too +#. this triggers an additional notification +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:616 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:622 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:635 +#: ../wallace/module_invitationpolicy.py:925 +#, python-format +msgid "\"%s\" has been updated" +msgstr "" + +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:627 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639 +msgid "PENDING" +msgstr "" + +#: ../wallace/__init__.py:57 +#, python-format +msgid "Wallace modules: %r" +msgstr "" + +#: ../wallace/__init__.py:69 +#, python-format +msgid "Module %s.execute() failed on message %r with error: %s" +msgstr "" + +#: ../wallace/__init__.py:78 #, python-format msgid "Worker process %s initializing" msgstr "" -#: ../wallace/__init__.py:81 +#: ../wallace/__init__.py:100 msgid "Bind address for Wallace." msgstr "" -#: ../wallace/__init__.py:107 +#: ../wallace/__init__.py:126 msgid "Port that Wallace is supposed to use." msgstr "" -#: ../wallace/__init__.py:158 +#: ../wallace/__init__.py:177 #, python-format msgid "Could not bind to socket on port %d on bind " msgstr "" -#: ../wallace/__init__.py:170 +#: ../wallace/__init__.py:189 msgid "Could not shut down socket" msgstr "" -#: ../wallace/__init__.py:227 +#: ../wallace/__init__.py:253 msgid "Accepted connection" msgstr "" -#: ../wallace/__init__.py:398 +#: ../wallace/__init__.py:428 #, python-format msgid "Could not write pid file %s" msgstr "" -#: ../wallace/module_optout.py:61 ../wallace/module_resources.py:93 +#: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60 +#: ../wallace/module_invitationpolicy.py:168 ../wallace/module_optout.py:61 +#: ../wallace/module_resources.py:120 #, python-format msgid "Issuing callback after processing to stage %s" msgstr "" -#: ../wallace/module_optout.py:62 ../wallace/module_resources.py:99 +#: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61 +#: ../wallace/module_invitationpolicy.py:170 ../wallace/module_optout.py:62 +#: ../wallace/module_resources.py:126 #, python-format msgid "Testing cb_action_%s()" msgstr "" -#: ../wallace/module_optout.py:64 ../wallace/module_resources.py:102 +#: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63 +#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:64 +#: ../wallace/module_resources.py:129 #, python-format msgid "Attempting to execute cb_action_%s()" msgstr "" +#: ../wallace/module_footer.py:67 +#, python-format +msgid "Executing module footer for %r, %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:66 +#, python-format +msgid "Executing module gpgencrypt for %r, %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:98 +msgid "Message is already encrypted (app/pgp-enc content-type)" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:102 +msgid "Message already encrypted by main content-type header" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:131 +msgid "" +"Configured to encrypt to a key not configured, and strict policy enabled. " +"Bailing out." +msgstr "" + +#: ../wallace/module_gpgencrypt.py:134 +msgid "" +"Configured to encrypt to a key not configured, but continuing anyway (see " +"'gpgencrypt_strict')." +msgstr "" + +#: ../wallace/module_gpgencrypt.py:171 +#, python-format +msgid "Recipients: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:183 +#, python-format +msgid "Current keys: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:188 +#, python-format +msgid "Retrieving key for recipient: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:192 ../wallace/module_gpgencrypt.py:208 +#, python-format +msgid "Found matching address %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:200 +#, python-format +msgid "Found matching address %r in remote keys" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:232 +#, python-format +msgid "An error occurred: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:154 +#, python-format +msgid "Invitation policy called for %r, %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:211 +#: ../wallace/module_resources.py:169 +#, python-format +msgid "Failed to parse iTip events from message: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:215 +msgid "" +"Message is not an iTip message or does not contain any (valid) iTip events." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:219 +#, python-format +msgid "" +"iTip events attached to this message contain the following information: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:232 +#, python-format +msgid "No itips, no users, pass along %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:235 +#, python-format +msgid "iTips, but no users, pass along %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:255 +#, python-format +msgid "No user attendee matching envelope recipient %s, skip message" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:259 +#, python-format +msgid "Receiving user: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:284 +#, python-format +msgid "Apply invitation policy %r for domain %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:295 +#, python-format +msgid "Ignoring '%s' iTip method" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:299 +#, python-format +msgid "iTip message %r consumed by the invitationpolicy module" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:315 +msgid "Pass invitation for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:320 +#, python-format +msgid "Receiving Attendee: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:339 +#, python-format +msgid "Existing event: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:350 +#, python-format +msgid "Precondition for event %r fulfilled: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:386 +#, python-format +msgid "No RSVP for recipient %r requested" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:412 +msgid "Pass reply for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:419 +#, python-format +msgid "Sender Attendee: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:431 +#, python-format +msgid "" +"The iTip reply sequence (%r) doesn't match the referred event version (%r). " +"Forwarding to Inbox." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:437 +#, python-format +msgid "Auto-updating event %r on iTip REPLY" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:459 +#: ../wallace/module_invitationpolicy.py:488 +msgid "" +"The event referred by this reply was not found in the user's calendars. " +"Forwarding to Inbox." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:472 +msgid "Pass cancellation for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:517 +#, python-format +msgid "Checking if email address %r belongs to a local user" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:522 +#, python-format +msgid "User DN: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:524 +#, python-format +msgid "No user record(s) found for %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:577 +#, python-format +msgid "User record doesn't have the mailbox attribute %r set" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:590 +#, python-format +msgid "IMAP proxy authentication failed: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:612 +#, python-format +msgid "List calendar folders for user %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:628 +#, python-format +msgid "IMAP metadata for %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:658 +#, python-format +msgid "Searching folder %r for event %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:670 +#: ../wallace/module_invitationpolicy.py:709 +#: ../wallace/module_resources.py:486 +#, python-format +msgid "Failed to parse event from message %s/%s: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:696 +#, python-format +msgid "Listing events from folder %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:715 +#, python-format +msgid "Existing event %r conflicts with invitation %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:722 +#: ../wallace/module_resources.py:344 +#, python-format +msgid "start: %r, end: %r, total: %r, messages: %d" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:748 +#, python-format +msgid "%r is locked, waiting..." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:811 +#, python-format +msgid "Failed to save event: no calendar folder found for user %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:814 +#, python-format +msgid "Save event %r to user calendar %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:827 +#, python-format +msgid "Failed to save event to user calendar at %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:843 +#, python-format +msgid "Delete event %r in %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:863 +#, python-format +msgid "Compose participation status summary for event %r to user %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:901 +#, python-format +msgid "" +"Waiting for more automated replies (got %d of %d); skipping notification" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:998 +#, python-format +msgid "Updated %s's copy of %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1001 +#, python-format +msgid "Attendee %s's copy of %r not found" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1004 +#, python-format +msgid "Attendee %r not found in LDAP" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1008 +#, python-format +msgid "" +"\n" +" %(name)s has %(status)s your invitation for %(summary)s.\n" +"\n" +" *** This is an automated response sent by the Kolab Invitation system ***\n" +" " +msgstr "" + #. modules.next_module('optout') #: ../wallace/module_optout.py:70 #, python-format @@ -2176,171 +3187,256 @@ msgstr "" msgid "Could not send request to optout_url %s" msgstr "" -#: ../wallace/module_resources.py:80 +#: ../wallace/module_resources.py:110 #, python-format msgid "Resource Management called for %r, %r" msgstr "" -#: ../wallace/module_resources.py:143 +#: ../wallace/module_resources.py:174 msgid "Message is not an iTip message or does not contain any " msgstr "" -#: ../wallace/module_resources.py:151 +#: ../wallace/module_resources.py:182 msgid "iTip events attached to this message contain the " msgstr "" -#: ../wallace/module_resources.py:171 +#: ../wallace/module_resources.py:205 msgid "Not an iTip message, but sent to resource nonetheless. Reject message" msgstr "" -#: ../wallace/module_resources.py:179 -msgid "No itips, no resources, pass along" +#: ../wallace/module_resources.py:213 +#, python-format +msgid "No itips, no resources, pass along %r" msgstr "" -#: ../wallace/module_resources.py:183 -msgid "iTips, but no resources, pass along" +#: ../wallace/module_resources.py:216 +#, python-format +msgid "iTips, but no resources, pass along %r" msgstr "" -#: ../wallace/module_resources.py:215 +#: ../wallace/module_resources.py:225 #, python-format -msgid "Resources: %r" +msgid "No resource attendees matching envelope recipient %s, Reject message" msgstr "" -#: ../wallace/module_resources.py:233 +#: ../wallace/module_resources.py:234 #, python-format -msgid "Checking events in resource folder %r" +msgid "Resources: %r; %r" msgstr "" -#: ../wallace/module_resources.py:240 +#: ../wallace/module_resources.py:244 #, python-format -msgid "Mailbox for resource %r doesn't exist" +msgid "Receiving Resource: %r; %r" msgstr "" -#: ../wallace/module_resources.py:253 +#: ../wallace/module_resources.py:252 #, python-format -msgid "Fetching message UID %r from folder %r" +msgid "Recipient %r is non-participant, ignoring message" msgstr "" -#: ../wallace/module_resources.py:292 +#: ../wallace/module_resources.py:279 #, python-format -msgid "Event %r conflicts with event " +msgid "Accept invitation for individual resource %r / %r" msgstr "" -#: ../wallace/module_resources.py:305 +#: ../wallace/module_resources.py:308 #, python-format -msgid "start: %r, end: %r, total: %r, messages: %r" +msgid "Delegate invitation for resource collection %r to %r" msgstr "" -#: ../wallace/module_resources.py:312 +#: ../wallace/module_resources.py:340 +#, python-format +msgid "Failed to read resource calendar for %r: %r" +msgstr "" + +#: ../wallace/module_resources.py:350 #, python-format msgid "Polling for resource %r" msgstr "" -#: ../wallace/module_resources.py:316 +#: ../wallace/module_resources.py:353 #, python-format msgid "Resource %r has been popped from the list" msgstr "" -#: ../wallace/module_resources.py:323 +#: ../wallace/module_resources.py:357 msgid "Resource is a collection" msgstr "" -#: ../wallace/module_resources.py:371 ../wallace/module_resources.py:421 +#: ../wallace/module_resources.py:368 #, python-format -msgid "Adding event to %r" +msgid "Removed conflicting resources from %r: (%r) => %r" msgstr "" -#: ../wallace/module_resources.py:470 +#: ../wallace/module_resources.py:380 #, python-format -msgid "Method %r not really interesting for us." +msgid "Conflicting events: %r for resource %r" msgstr "" -#: ../wallace/module_resources.py:478 +#: ../wallace/module_resources.py:397 #, python-format -msgid "Raw iTip payload: %s" +msgid "Delegate to another resource collection member: %r to %r" msgstr "" -#: ../wallace/module_resources.py:488 -msgid "Could not read iTip from message." +#: ../wallace/module_resources.py:459 +#, python-format +msgid "Checking events in resource folder %r" msgstr "" -#: ../wallace/module_resources.py:510 -msgid "iTip event without a start" +#: ../wallace/module_resources.py:475 +#, python-format +msgid "Fetching message UID %r from folder %r" msgstr "" -#. end if c.name == "VEVENT" -#. end for c in cal.walk() -#. end if part.get_content_type() == "text/calendar" -#. end for part in message.walk() -#. if message.is_multipart() -#: ../wallace/module_resources.py:540 -msgid "Message is not an iTip message (non-multipart message)" +#: ../wallace/module_resources.py:498 +#, python-format +msgid "Event %r conflicts with event %r" msgstr "" -#: ../wallace/module_resources.py:561 +#: ../wallace/module_resources.py:525 #, python-format -msgid "Checking if email address %r belongs to a resource (collection)" +msgid "Adding event to %r: %r" msgstr "" -#: ../wallace/module_resources.py:572 ../wallace/module_resources.py:646 -#: ../wallace/module_resources.py:696 +#: ../wallace/module_resources.py:573 #, python-format -msgid "No resource (collection) records found for %r" +msgid "Failed to save event to resource calendar at %r: %r" +msgstr "" + +#: ../wallace/module_resources.py:590 +#, python-format +msgid "Delete resource calendar object %r in %r: %r" +msgstr "" + +#: ../wallace/module_resources.py:633 +#, python-format +msgid "Checking if email address %r belongs to a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:580 ../wallace/module_resources.py:654 -#: ../wallace/module_resources.py:704 +#: ../wallace/module_resources.py:641 ../wallace/module_resources.py:709 +#: ../wallace/module_resources.py:743 #, python-format msgid "Resource record(s): %r" msgstr "" -#: ../wallace/module_resources.py:586 ../wallace/module_resources.py:661 -#: ../wallace/module_resources.py:711 +#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711 +#: ../wallace/module_resources.py:746 +#, python-format +msgid "No resource (collection) records found for %r" +msgstr "" + +#: ../wallace/module_resources.py:647 ../wallace/module_resources.py:715 +#: ../wallace/module_resources.py:750 #, python-format msgid "Resource record: %r" msgstr "" -#: ../wallace/module_resources.py:605 +#: ../wallace/module_resources.py:667 #, python-format msgid "Raw itip_events: %r" msgstr "" -#: ../wallace/module_resources.py:613 +#: ../wallace/module_resources.py:675 #, python-format msgid "Raw set of attendees: %r" msgstr "" -#: ../wallace/module_resources.py:621 +#: ../wallace/module_resources.py:683 #, python-format msgid "Raw set of resources: %r" msgstr "" -#: ../wallace/module_resources.py:635 +#: ../wallace/module_resources.py:702 #, python-format msgid "Checking if attendee %r is a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:668 ../wallace/module_resources.py:714 +#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752 msgid "Resource reservation made but no resource records found" msgstr "" -#: ../wallace/module_resources.py:686 +#: ../wallace/module_resources.py:737 #, python-format msgid "Checking if resource %r is a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:718 +#: ../wallace/module_resources.py:755 msgid "The following resources are being referred to in the " msgstr "" +#: ../wallace/module_resources.py:894 +#, python-format +msgid "" +"\n" +" *** This is an automated response, please do not reply! ***\n" +"\n" +" Your reservation was delegated to \"%s\" which is available for the requested time.\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:905 +#, python-format +msgid "" +"\n" +" *** This is an automated response, please do not reply! ***\n" +" \n" +" We hereby inform you that your reservation was %s.\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:912 +#, python-format +msgid "" +"\n" +" If you have questions about this reservation, please contact\n" +" %s <%s> %s\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:941 +#, python-format +msgid "Sending booking notification for event %r to %r from %r" +msgstr "" + +#: ../wallace/module_resources.py:954 +msgid "failed" +msgstr "" + +#: ../wallace/module_resources.py:973 +#, python-format +msgid "" +"\n" +" The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s.\n" +"\n" +" *** This is an automated message, sent to you as the resource owner. ***\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:979 +#, python-format +msgid "" +"\n" +" A reservation request for %(resource)s could not be processed automatically.\n" +" Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s.\n" +"\n" +" *** This is an automated message, sent to you as the resource owner. ***\n" +" " +msgstr "" + #. This is a nested module #: ../wallace/modules.py:97 #, python-format msgid "Module Group: %s" msgstr "" -#: ../wallace/modules.py:108 ../wallace/modules.py:113 -msgid "No such module." +#: ../wallace/modules.py:108 +#, python-format +msgid "No such module %r in modules %r (1)." +msgstr "" + +#: ../wallace/modules.py:113 +#, python-format +msgid "No such module %r in modules %r (2)." msgstr "" #: ../wallace/modules.py:119 @@ -2353,33 +3449,33 @@ msgstr "" msgid "Deferring message in %s (by module %s)" msgstr "" -#: ../wallace/modules.py:133 +#: ../wallace/modules.py:134 #, python-format msgid "The time when the message was sent: %r" msgstr "" -#: ../wallace/modules.py:134 +#: ../wallace/modules.py:135 #, python-format msgid "The time now: %r" msgstr "" -#: ../wallace/modules.py:135 +#: ../wallace/modules.py:136 #, python-format msgid "The time delta: %r" msgstr "" #. TODO: Send NDR back to user -#: ../wallace/modules.py:139 +#: ../wallace/modules.py:140 #, python-format msgid "Message in file %s older then 5 days, deleting" msgstr "" -#: ../wallace/modules.py:164 +#: ../wallace/modules.py:165 #, python-format msgid "Rejecting message in %s (by module %s)" msgstr "" -#: ../wallace/modules.py:185 +#: ../wallace/modules.py:186 #, python-format msgid "" "This is the email system Wallace at %s.\n" @@ -2394,19 +3490,29 @@ msgid "" "recipients.\n" msgstr "" -#: ../wallace/modules.py:200 +#: ../wallace/modules.py:201 #, python-format msgid "" "X-Wallace-Module: %s\n" "X-Wallace-Result: REJECT\n" msgstr "" -#: ../wallace/modules.py:253 +#: ../wallace/modules.py:260 #, python-format msgid "Accepting message in %s (by module %s)" msgstr "" -#: ../wallace/modules.py:326 +#: ../wallace/modules.py:262 +#, python-format +msgid "Accepting message in: %r" +msgstr "" + +#: ../wallace/modules.py:269 +#, python-format +msgid "recipients: %r" +msgstr "" + +#: ../wallace/modules.py:347 #, python-format msgid "Module '%s' already registered" msgstr "" diff --git a/po/pykolab.pot b/po/pykolab.pot index c3c276a..389ca9b 100644 --- a/po/pykolab.pot +++ b/po/pykolab.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-02-21 02:15-0500\n" +"POT-Creation-Date: 2014-07-10 07:21-0400\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -291,66 +291,66 @@ msgstr "" msgid "No objections" msgstr "" -#: ../conf.py:37 ../kolab.py:34 ../saslauthd.py:33 +#: ../conf.py:37 ../kolab-cli.py:34 ../saslauthd.py:33 msgid "Cannot load pykolab/logger.py:" msgstr "" #: ../kolabd/__init__.py:49 ../saslauthd/__init__.py:51 -#: ../wallace/__init__.py:68 +#: ../wallace/__init__.py:85 msgid "Daemon Options" msgstr "" #: ../kolabd/__init__.py:56 ../saslauthd/__init__.py:58 -#: ../wallace/__init__.py:75 +#: ../wallace/__init__.py:92 msgid "Fork to the background." msgstr "" #: ../kolabd/__init__.py:65 ../saslauthd/__init__.py:67 -#: ../wallace/__init__.py:101 +#: ../wallace/__init__.py:118 msgid "Path to the PID file to use." msgstr "" #: ../kolabd/__init__.py:74 ../saslauthd/__init__.py:76 -#: ../wallace/__init__.py:118 +#: ../wallace/__init__.py:135 msgid "Run as user USERNAME" msgstr "" #: ../kolabd/__init__.py:84 ../saslauthd/__init__.py:86 -#: ../wallace/__init__.py:92 +#: ../wallace/__init__.py:109 msgid "Run as group GROUPNAME" msgstr "" #: ../kolabd/__init__.py:122 ../pykolab/logger.py:139 ../pykolab/utils.py:234 -#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:312 +#: ../saslauthd/__init__.py:292 ../wallace/__init__.py:329 #, python-format msgid "Group %s does not exist" msgstr "" #: ../kolabd/__init__.py:131 ../saslauthd/__init__.py:301 -#: ../wallace/__init__.py:321 +#: ../wallace/__init__.py:338 #, python-format msgid "Switching real and effective group id to %d" msgstr "" #: ../kolabd/__init__.py:153 ../pykolab/logger.py:159 ../pykolab/utils.py:258 -#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:343 +#: ../saslauthd/__init__.py:323 ../wallace/__init__.py:360 #, python-format msgid "User %s does not exist" msgstr "" #: ../kolabd/__init__.py:163 ../saslauthd/__init__.py:333 -#: ../wallace/__init__.py:353 +#: ../wallace/__init__.py:370 #, python-format msgid "Switching real and effective user id to %d" msgstr "" #: ../kolabd/__init__.py:172 ../saslauthd/__init__.py:342 -#: ../wallace/__init__.py:362 +#: ../wallace/__init__.py:379 msgid "Could not change real and effective uid and/or gid" msgstr "" #: ../kolabd/__init__.py:192 ../saslauthd/__init__.py:133 -#: ../wallace/__init__.py:382 +#: ../wallace/__init__.py:399 msgid "Interrupted by user" msgstr "" @@ -359,7 +359,7 @@ msgid "Traceback occurred, please report a " msgstr "" #: ../kolabd/__init__.py:203 ../saslauthd/__init__.py:141 -#: ../wallace/__init__.py:391 +#: ../wallace/__init__.py:408 #, python-format msgid "Type Error: %s" msgstr "" @@ -368,7 +368,7 @@ msgstr "" msgid "Could not connect to LDAP, is it running?" msgstr "" -#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2110 +#: ../kolabd/__init__.py:233 ../pykolab/auth/ldap/__init__.py:2137 #: ../pykolab/cli/cmd_sync.py:36 msgid "Listing domains..." msgstr "" @@ -442,7 +442,7 @@ msgstr "" msgid "Updating timestamp for cache entry %r" msgstr "" -#: ../pykolab/auth/ldap/cache.py:154 +#: ../pykolab/auth/ldap/cache.py:155 #, python-format msgid "Updating result_attribute for cache entry %r" msgstr "" @@ -668,95 +668,95 @@ msgstr "" msgid "Invalid DN, username and/or password." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1234 ../pykolab/auth/ldap/__init__.py:1247 -#: ../pykolab/auth/ldap/__init__.py:1603 ../pykolab/auth/ldap/__init__.py:1616 +#: ../pykolab/auth/ldap/__init__.py:1236 ../pykolab/auth/ldap/__init__.py:1249 +#: ../pykolab/auth/ldap/__init__.py:1614 ../pykolab/auth/ldap/__init__.py:1627 #, python-format msgid "Found a subject %r with access %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1354 +#: ../pykolab/auth/ldap/__init__.py:1356 #, python-format msgid "Entry %s attribute value: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1362 +#: ../pykolab/auth/ldap/__init__.py:1364 #, python-format msgid "imap.user_mailbox_server(%r) result: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1673 ../pykolab/auth/ldap/__init__.py:1830 +#: ../pykolab/auth/ldap/__init__.py:1684 ../pykolab/auth/ldap/__init__.py:1853 #, python-format msgid "Result from recipient policy: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:1885 +#: ../pykolab/auth/ldap/__init__.py:1908 #, python-format msgid "Kolab user %s does not have a result attribute %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2040 +#: ../pykolab/auth/ldap/__init__.py:2067 #, python-format msgid "Finding domain root dn for domain %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2137 +#: ../pykolab/auth/ldap/__init__.py:2164 msgid "Authentication database DOWN" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2221 ../pykolab/auth/ldap/__init__.py:2269 +#: ../pykolab/auth/ldap/__init__.py:2248 ../pykolab/auth/ldap/__init__.py:2296 #, python-format msgid "Entry type: %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2294 +#: ../pykolab/auth/ldap/__init__.py:2321 #, python-format msgid "Done with _synchronize_callback() for entry %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2366 +#: ../pykolab/auth/ldap/__init__.py:2393 msgid "LDAP Search Result Data Entry:" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2382 +#: ../pykolab/auth/ldap/__init__.py:2409 msgid "Entry Change Notification attributes:" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2387 +#: ../pykolab/auth/ldap/__init__.py:2414 #, python-format msgid "Change Type: %r (%r)" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2395 +#: ../pykolab/auth/ldap/__init__.py:2422 #, python-format msgid "Previous DN: %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2450 +#: ../pykolab/auth/ldap/__init__.py:2477 #, python-format msgid "Object %s searched no longer exists" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2460 +#: ../pykolab/auth/ldap/__init__.py:2487 #, python-format msgid "%d results..." msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2563 +#: ../pykolab/auth/ldap/__init__.py:2590 #, python-format msgid "Searching with filter %r" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2615 +#: ../pykolab/auth/ldap/__init__.py:2642 #, python-format msgid "Checking for support for %s on %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2634 +#: ../pykolab/auth/ldap/__init__.py:2661 #, python-format msgid "Found support for %s" msgstr "" -#: ../pykolab/auth/ldap/__init__.py:2679 +#: ../pykolab/auth/ldap/__init__.py:2706 #, python-format msgid "An error occured using %s: %r" msgstr "" @@ -916,7 +916,10 @@ msgstr "" #: ../pykolab/cli/cmd_list_mailbox_metadata.py:54 #: ../pykolab/cli/cmd_set_mailbox_acl.py:54 #: ../pykolab/cli/cmd_set_mailbox_metadata.py:66 -#: ../pykolab/cli/cmd_set_quota.py:46 +#: ../pykolab/cli/cmd_set_quota.py:46 ../tests/unit/test-015-translate.py:12 +#: ../tests/unit/test-015-translate.py:16 +#: ../tests/unit/test-015-translate.py:18 +#: ../tests/unit/test-015-translate.py:20 msgid "Folder name" msgstr "" @@ -1063,7 +1066,7 @@ msgstr "" #: ../pykolab/cli/cmd_remove_user_subscription.py:92 #, python-format -msgid "User %s not be unsubscribed from any folders." +msgid "User %s was not unsubscribed from any folders." msgstr "" #: ../pykolab/cli/cmd_rename_mailbox.py:52 @@ -1432,76 +1435,76 @@ msgstr "" msgid "No command supplied" msgstr "" -#: ../pykolab/conf/__init__.py:411 +#: ../pykolab/conf/__init__.py:416 msgid "Insufficient options. Need section, key and value -in that order." msgstr "" -#: ../pykolab/conf/__init__.py:414 +#: ../pykolab/conf/__init__.py:419 #, python-format msgid "No section '%s' exists." msgstr "" -#: ../pykolab/conf/__init__.py:445 +#: ../pykolab/conf/__init__.py:461 #, python-format msgid "Setting %s to %r (from the default values for CLI options)" msgstr "" -#: ../pykolab/conf/__init__.py:518 +#: ../pykolab/conf/__init__.py:534 #, python-format msgid "Could not execute configuration function: %s" msgstr "" -#: ../pykolab/conf/__init__.py:526 +#: ../pykolab/conf/__init__.py:542 #, python-format msgid "Option %s/%s does not exist in config file %s, pulling from defaults" msgstr "" -#: ../pykolab/conf/__init__.py:534 ../pykolab/conf/__init__.py:537 +#: ../pykolab/conf/__init__.py:550 ../pykolab/conf/__init__.py:553 msgid "Option does not exist in defaults." msgstr "" -#: ../pykolab/conf/__init__.py:547 +#: ../pykolab/conf/__init__.py:563 #, python-format msgid "Configuration file %s not readable." msgstr "" -#: ../pykolab/conf/__init__.py:550 +#: ../pykolab/conf/__init__.py:566 #, python-format msgid "Configuration file %s does not exist." msgstr "" -#: ../pykolab/conf/__init__.py:555 +#: ../pykolab/conf/__init__.py:571 msgid "" "WARNING: A negative debug level value does not make this program be any more " "silent." msgstr "" -#: ../pykolab/conf/__init__.py:561 +#: ../pykolab/conf/__init__.py:577 msgid "This program has 9 levels of verbosity. Using the maximum of 9." msgstr "" -#: ../pykolab/conf/__init__.py:569 ../pykolab/conf/__init__.py:575 +#: ../pykolab/conf/__init__.py:585 ../pykolab/conf/__init__.py:591 msgid "Cannot start SASL authentication daemon" msgstr "" -#: ../pykolab/conf/__init__.py:586 +#: ../pykolab/conf/__init__.py:602 msgid "No imaplib library found." msgstr "" -#: ../pykolab/conf/__init__.py:596 +#: ../pykolab/conf/__init__.py:612 msgid "No LMTP class found in the smtplib library." msgstr "" -#: ../pykolab/conf/__init__.py:606 +#: ../pykolab/conf/__init__.py:622 msgid "No SMTP class found in the smtplib library." msgstr "" -#: ../pykolab/conf/__init__.py:620 +#: ../pykolab/conf/__init__.py:636 #, python-format msgid "Found you specified a specific set of items to test: %s" msgstr "" -#: ../pykolab/conf/__init__.py:628 +#: ../pykolab/conf/__init__.py:644 #, python-format msgid "Selectively selecting: %s" msgstr "" @@ -1686,169 +1689,174 @@ msgstr "" msgid "%r has no attribute %s" msgstr "" -#: ../pykolab/imap/__init__.py:390 ../pykolab/imap/__init__.py:425 +#: ../pykolab/imap/__init__.py:393 ../pykolab/imap/__init__.py:428 #, python-format msgid "Creating new shared folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:450 ../pykolab/imap/__init__.py:672 +#: ../pykolab/imap/__init__.py:453 ../pykolab/imap/__init__.py:675 #, python-format msgid "Downcasing mailbox name %r" msgstr "" -#: ../pykolab/imap/__init__.py:454 +#: ../pykolab/imap/__init__.py:457 #, python-format msgid "Creating new mailbox for user %s" msgstr "" -#: ../pykolab/imap/__init__.py:467 +#: ../pykolab/imap/__init__.py:470 msgid "Waiting for the Cyrus IMAP Murder to settle..." msgstr "" -#: ../pykolab/imap/__init__.py:513 +#: ../pykolab/imap/__init__.py:516 #, python-format msgid "Creating additional folders for user %s" msgstr "" -#: ../pykolab/imap/__init__.py:532 +#: ../pykolab/imap/__init__.py:535 #, python-format msgid "Waiting for the Cyrus murder to settle... %r" msgstr "" -#: ../pykolab/imap/__init__.py:544 +#: ../pykolab/imap/__init__.py:547 #, python-format msgid "Correcting additional folder name from %r to %r" msgstr "" -#: ../pykolab/imap/__init__.py:550 +#: ../pykolab/imap/__init__.py:553 #, python-format msgid "Mailbox already exists: %s" msgstr "" -#: ../pykolab/imap/__init__.py:590 +#: ../pykolab/imap/__init__.py:593 msgid "Subscribing user to the additional folders" msgstr "" -#: ../pykolab/imap/__init__.py:604 +#: ../pykolab/imap/__init__.py:607 msgid "Using the following tests for folder subscriptions:" msgstr "" -#: ../pykolab/imap/__init__.py:606 +#: ../pykolab/imap/__init__.py:609 #, python-format msgid " %r" msgstr "" -#: ../pykolab/imap/__init__.py:609 +#: ../pykolab/imap/__init__.py:612 #, python-format msgid "Folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:621 +#: ../pykolab/imap/__init__.py:624 #, python-format msgid "Subscribing %s to folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:625 +#: ../pykolab/imap/__init__.py:628 #, python-format msgid "Subscribing %s to folder %s failed: %r" msgstr "" -#: ../pykolab/imap/__init__.py:655 +#: ../pykolab/imap/__init__.py:658 #, python-format msgid "Could not rename %s to reside on partition %s" msgstr "" -#: ../pykolab/imap/__init__.py:688 ../pykolab/imap/__init__.py:764 +#: ../pykolab/imap/__init__.py:691 +#, python-format +msgid "INBOX folder to rename (%s) does not exist" +msgstr "" + +#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:770 #, python-format msgid "Renaming INBOX from %s to %s" msgstr "" -#: ../pykolab/imap/__init__.py:692 +#: ../pykolab/imap/__init__.py:698 #, python-format msgid "Could not rename INBOX folder %s to %s" msgstr "" -#: ../pykolab/imap/__init__.py:694 ../pykolab/imap/__init__.py:768 +#: ../pykolab/imap/__init__.py:700 ../pykolab/imap/__init__.py:774 #, python-format msgid "Moving INBOX folder %s won't succeed as target folder %s already exists" msgstr "" -#: ../pykolab/imap/__init__.py:698 +#: ../pykolab/imap/__init__.py:704 #, python-format msgid "Server for mailbox %r is %r" msgstr "" -#: ../pykolab/imap/__init__.py:706 +#: ../pykolab/imap/__init__.py:712 #, python-format msgid "Looking for folder '%s', we found folders: %r" msgstr "" -#: ../pykolab/imap/__init__.py:729 +#: ../pykolab/imap/__init__.py:735 #, python-format msgid "Setting ACL rights %s for subject %s on folder " msgstr "" -#: ../pykolab/imap/__init__.py:740 +#: ../pykolab/imap/__init__.py:746 #, python-format msgid "Removing ACL rights %s for subject %s on folder " msgstr "" -#: ../pykolab/imap/__init__.py:761 +#: ../pykolab/imap/__init__.py:767 #, python-format msgid "Found old INBOX folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:770 +#: ../pykolab/imap/__init__.py:776 #, python-format msgid "Did not find old folder user/%s to rename" msgstr "" -#: ../pykolab/imap/__init__.py:772 +#: ../pykolab/imap/__init__.py:778 msgid "Value for user is not a dictionary" msgstr "" #. TODO: Go in fact correct the quota. -#: ../pykolab/imap/__init__.py:840 +#: ../pykolab/imap/__init__.py:846 #, python-format msgid "Cannot get current IMAP quota for folder %s" msgstr "" -#: ../pykolab/imap/__init__.py:853 +#: ../pykolab/imap/__init__.py:859 #, python-format msgid "Quota for %s currently is %s" msgstr "" -#: ../pykolab/imap/__init__.py:859 +#: ../pykolab/imap/__init__.py:865 #, python-format msgid "Adjusting authentication database quota for folder %s to %d" msgstr "" -#: ../pykolab/imap/__init__.py:864 +#: ../pykolab/imap/__init__.py:870 #, python-format msgid "Correcting quota for %s to %s (currently %s)" msgstr "" -#: ../pykolab/imap/__init__.py:941 +#: ../pykolab/imap/__init__.py:947 #, python-format msgid "Checking folder: %s" msgstr "" -#: ../pykolab/imap/__init__.py:946 +#: ../pykolab/imap/__init__.py:952 #, python-format msgid "Folder has no corresponding user (1): %s" msgstr "" -#: ../pykolab/imap/__init__.py:949 +#: ../pykolab/imap/__init__.py:955 #, python-format msgid "Folder has no corresponding user (2): %s" msgstr "" #. We got user identifier only -#: ../pykolab/imap/__init__.py:964 +#: ../pykolab/imap/__init__.py:970 msgid "Please don't give us just a user identifier" msgstr "" -#: ../pykolab/imap/__init__.py:967 +#: ../pykolab/imap/__init__.py:973 #, python-format msgid "Deleting folder %s" msgstr "" @@ -1857,6 +1865,44 @@ msgstr "" msgid "Returning thread local configuration" msgstr "" +#: ../pykolab/itip/__init__.py:43 +#, python-format +msgid "Method %r not really interesting for us." +msgstr "" + +#: ../pykolab/itip/__init__.py:49 +#, python-format +msgid "Raw iTip payload: %s" +msgstr "" + +#: ../pykolab/itip/__init__.py:59 +msgid "Could not read iTip from message." +msgstr "" + +#: ../pykolab/itip/__init__.py:67 +#, python-format +msgid "Duplicate iTip object: %s" +msgstr "" + +#: ../pykolab/itip/__init__.py:90 +msgid "iTip event without a start" +msgstr "" + +#: ../pykolab/itip/__init__.py:132 +msgid "Message is not an iTip message (non-multipart message)" +msgstr "" + +#: ../pykolab/itip/__init__.py:225 +#, python-format +msgid "Failed to compose iTip reply message: %r" +msgstr "" + +#: ../pykolab/itip/__init__.py:236 ../wallace/module_invitationpolicy.py:936 +#: ../wallace/module_resources.py:964 +#, python-format +msgid "SMTP sendmail error: %r" +msgstr "" + #: ../pykolab/logger.py:173 ../pykolab/logger.py:179 #, python-format msgid "Could not change permissions on %s: %r" @@ -1991,6 +2037,16 @@ msgid "" "Attribute substitution for 'alternative_mail' failed in Recipient Policy" msgstr "" +#: ../pykolab/plugins/roundcubedb/__init__.py:48 +#, python-format +msgid "user_delete: %r" +msgstr "" + +#: ../pykolab/plugins/roundcubedb/__init__.py:55 +#: ../pykolab/setup/setup_roundcube.py:160 +msgid "Roundcube installation path not found." +msgstr "" + #: ../pykolab/plugins/sievemgmt/__init__.py:51 msgid "Wrong number of arguments for sieve management plugin" msgstr "" @@ -2007,11 +2063,11 @@ msgstr "" msgid "No such component." msgstr "" -#: ../pykolab/setup/setup_freebusy.py:45 +#: ../pykolab/setup/setup_freebusy.py:46 msgid "Setup Free/Busy." msgstr "" -#: ../pykolab/setup/setup_freebusy.py:49 +#: ../pykolab/setup/setup_freebusy.py:50 msgid "Free/Busy is not installed on this system" msgstr "" @@ -2033,7 +2089,7 @@ msgstr "" #: ../pykolab/setup/setup_imap.py:173 ../pykolab/setup/setup_kolabd.py:81 #: ../pykolab/setup/setup_ldap.py:426 ../pykolab/setup/setup_mta.py:455 -#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:234 +#: ../pykolab/setup/setup_mysql.py:58 ../pykolab/setup/setup_roundcube.py:237 #: ../pykolab/setup/setup_syncroton.py:102 msgid "Could not configure to start on boot, the " msgstr "" @@ -2371,10 +2427,6 @@ msgstr "" msgid "Could not start the MySQL database service." msgstr "" -#. Regular old-fashioned Enterprise Linux -#. Debian -#. (open)SUSE -#. "Unbreakable" Linux from Oracle #: ../pykolab/setup/setup_mysql.py:71 msgid "What MySQL server are we setting up?" msgstr "" @@ -2390,7 +2442,7 @@ msgid "" msgstr "" #: ../pykolab/setup/setup_mysql.py:82 ../pykolab/setup/setup_mysql.py:99 -#: ../pykolab/setup/setup_roundcube.py:180 +#: ../pykolab/setup/setup_roundcube.py:183 #: ../pykolab/setup/setup_syncroton.py:63 msgid "MySQL root password" msgstr "" @@ -2486,21 +2538,17 @@ msgstr "" msgid "MySQL roundcube password" msgstr "" -#: ../pykolab/setup/setup_roundcube.py:117 +#: ../pykolab/setup/setup_roundcube.py:120 #, python-format msgid "Using template file %r" msgstr "" -#: ../pykolab/setup/setup_roundcube.py:124 +#: ../pykolab/setup/setup_roundcube.py:127 #, python-format msgid "Successfully compiled template %r, writing out to %r" msgstr "" -#: ../pykolab/setup/setup_roundcube.py:157 -msgid "Roundcube installation path not found." -msgstr "" - -#: ../pykolab/setup/setup_roundcube.py:225 +#: ../pykolab/setup/setup_roundcube.py:228 #: ../pykolab/setup/setup_syncroton.py:93 msgid "Could not start the webserver server service." msgstr "" @@ -2570,29 +2618,29 @@ msgstr "" msgid "Could not change the permissions on %s" msgstr "" -#: ../pykolab/utils.py:476 +#: ../pykolab/utils.py:479 #, python-format msgid "Transliterating string %r with locale %r" msgstr "" -#: ../pykolab/utils.py:484 +#: ../pykolab/utils.py:487 msgid "Attempting to set locale" msgstr "" -#: ../pykolab/utils.py:486 +#: ../pykolab/utils.py:489 msgid "Success setting locale" msgstr "" -#: ../pykolab/utils.py:488 +#: ../pykolab/utils.py:491 msgid "Failure to set locale" msgstr "" -#: ../pykolab/utils.py:496 +#: ../pykolab/utils.py:499 #, python-format msgid "Executing '%s | %s'" msgstr "" -#: ../pykolab/utils.py:507 +#: ../pykolab/utils.py:510 #, python-format msgid "Could not translate %s using locale %s" msgstr "" @@ -2608,90 +2656,124 @@ msgid "Got response: %r" msgstr "" #. Some data is not JSON -#: ../pykolab/wap_client/__init__.py:333 +#: ../pykolab/wap_client/__init__.py:334 msgid "Response data is not JSON" msgstr "" -#: ../pykolab/xml/attendee.py:86 ../pykolab/xml/attendee.py:108 +#. support integer values, too +#: ../pykolab/xml/attendee.py:9 ../pykolab/xml/attendee.py:17 +msgid "Needs Action" +msgstr "" + +#: ../pykolab/xml/attendee.py:10 ../pykolab/xml/attendee.py:18 +msgid "Accepted" +msgstr "" + +#: ../pykolab/xml/attendee.py:11 ../pykolab/xml/attendee.py:19 +msgid "Declined" +msgstr "" + +#: ../pykolab/xml/attendee.py:12 ../pykolab/xml/attendee.py:20 +msgid "Tentatively Accepted" +msgstr "" + +#: ../pykolab/xml/attendee.py:13 ../pykolab/xml/attendee.py:21 +msgid "Delegated" +msgstr "" + +#: ../pykolab/xml/attendee.py:14 +msgid "Completed" +msgstr "" + +#: ../pykolab/xml/attendee.py:15 +msgid "In Process" +msgstr "" + +#: ../pykolab/xml/attendee.py:108 ../pykolab/xml/attendee.py:130 msgid "Not a valid attendee" msgstr "" -#: ../pykolab/xml/attendee.py:93 +#: ../pykolab/xml/attendee.py:115 msgid "No valid delegator references found" msgstr "" -#: ../pykolab/xml/attendee.py:113 +#: ../pykolab/xml/attendee.py:135 msgid "No valid delegatee references found" msgstr "" -#: ../pykolab/xml/attendee.py:149 +#: ../pykolab/xml/attendee.py:180 #, python-format msgid "Invalid cutype %r" msgstr "" -#: ../pykolab/xml/attendee.py:160 +#: ../pykolab/xml/attendee.py:192 #, python-format msgid "Invalid participant status %r" msgstr "" -#: ../pykolab/xml/attendee.py:168 +#: ../pykolab/xml/attendee.py:200 #, python-format msgid "Invalid role %r" msgstr "" -#: ../pykolab/xml/event.py:70 ../pykolab/xml/event.py:570 -#: ../pykolab/xml/event.py:606 +#: ../pykolab/xml/event.py:100 ../pykolab/xml/event.py:708 +#: ../pykolab/xml/event.py:751 msgid "Event start needs datetime.date or datetime.datetime instance" msgstr "" -#: ../pykolab/xml/event.py:198 +#: ../pykolab/xml/event.py:241 #, python-format msgid "No attendee with email or name %r" msgstr "" -#: ../pykolab/xml/event.py:206 +#: ../pykolab/xml/event.py:249 #, python-format msgid "Invalid argument value attendee %r, must be basestring or Attendee" msgstr "" -#: ../pykolab/xml/event.py:212 +#: ../pykolab/xml/event.py:255 #, python-format msgid "No attendee with email %r" msgstr "" -#: ../pykolab/xml/event.py:218 +#: ../pykolab/xml/event.py:261 #, python-format msgid "No attendee with name %r" msgstr "" -#: ../pykolab/xml/event.py:323 +#: ../pykolab/xml/event.py:426 msgid "Invalid participant status" msgstr "" -#: ../pykolab/xml/event.py:445 -msgid "Event end needs datetime.date or datetime.datetime instance" +#: ../pykolab/xml/event.py:542 +#, python-format +msgid "Invalid status %r" msgstr "" -#: ../pykolab/xml/event.py:549 +#: ../pykolab/xml/event.py:550 #, python-format -msgid "Invalid status %r" +msgid "Invalid classification %r" msgstr "" -#: ../pykolab/xml/event.py:616 +#: ../pykolab/xml/event.py:577 +msgid "Event end needs datetime.date or datetime.datetime instance" +msgstr "" + +#: ../pykolab/xml/event.py:761 #, python-format msgid "Invalid status set: %r" msgstr "" -#: ../pykolab/xml/event.py:735 +#: ../pykolab/xml/event.py:923 msgid "No sender specified" msgstr "" -#: ../pykolab/xml/event.py:744 +#: ../pykolab/xml/event.py:932 #, python-format -msgid "Reservation Request for %s was %s" +msgid "Invitation for %s was %s" msgstr "" -#: ../pykolab/xml/event.py:749 +#: ../pykolab/xml/event.py:937 msgid "This is an automated response to one of your event requests." msgstr "" @@ -2701,7 +2783,7 @@ msgid "Could not create %r: %r" msgstr "" #: ../saslauthd/__init__.py:137 ../saslauthd/__init__.py:145 -#: ../wallace/__init__.py:386 ../wallace/__init__.py:395 +#: ../wallace/__init__.py:403 ../wallace/__init__.py:412 msgid "Traceback occurred, please report a bug at http://bugzilla.kolabsys.com" msgstr "" @@ -2713,51 +2795,101 @@ msgstr "" msgid "Maximum tries exceeded, exiting" msgstr "" -#: ../wallace/__init__.py:61 +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:190 +#: ../wallace/module_resources.py:879 +#, python-format +msgid "Reservation Request for %(summary)s was %(status)s" +msgstr "" + +#. check notification message sent to resource owner (jane) +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:605 +#: ../tests/functional/test_wallace/test_005_resource_invitation.py:621 +#: ../wallace/module_resources.py:954 +#, python-format +msgid "Booking for %s has been %s" +msgstr "" + +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:146 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:720 +#: ../wallace/module_invitationpolicy.py:374 +#, python-format +msgid "\"%(summary)s\" has been %(status)s" +msgstr "" + +#. check for notification message +#. this notification should be suppressed until mark has replied, too +#. this triggers an additional notification +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:616 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:622 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:635 +#: ../wallace/module_invitationpolicy.py:925 +#, python-format +msgid "\"%s\" has been updated" +msgstr "" + +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:627 +#: ../tests/functional/test_wallace/test_007_invitationpolicy.py:639 +msgid "PENDING" +msgstr "" + +#: ../wallace/__init__.py:57 +#, python-format +msgid "Wallace modules: %r" +msgstr "" + +#: ../wallace/__init__.py:69 +#, python-format +msgid "Module %s.execute() failed on message %r with error: %s" +msgstr "" + +#: ../wallace/__init__.py:78 #, python-format msgid "Worker process %s initializing" msgstr "" -#: ../wallace/__init__.py:83 +#: ../wallace/__init__.py:100 msgid "Bind address for Wallace." msgstr "" -#: ../wallace/__init__.py:109 +#: ../wallace/__init__.py:126 msgid "Port that Wallace is supposed to use." msgstr "" -#: ../wallace/__init__.py:160 +#: ../wallace/__init__.py:177 #, python-format msgid "Could not bind to socket on port %d on bind " msgstr "" -#: ../wallace/__init__.py:172 +#: ../wallace/__init__.py:189 msgid "Could not shut down socket" msgstr "" -#: ../wallace/__init__.py:236 +#: ../wallace/__init__.py:253 msgid "Accepted connection" msgstr "" -#: ../wallace/__init__.py:411 +#: ../wallace/__init__.py:428 #, python-format msgid "Could not write pid file %s" msgstr "" -#: ../wallace/module_footer.py:60 ../wallace/module_optout.py:61 -#: ../wallace/module_resources.py:109 +#: ../wallace/module_footer.py:60 ../wallace/module_gpgencrypt.py:60 +#: ../wallace/module_invitationpolicy.py:168 ../wallace/module_optout.py:61 +#: ../wallace/module_resources.py:120 #, python-format msgid "Issuing callback after processing to stage %s" msgstr "" -#: ../wallace/module_footer.py:61 ../wallace/module_optout.py:62 -#: ../wallace/module_resources.py:115 +#: ../wallace/module_footer.py:61 ../wallace/module_gpgencrypt.py:61 +#: ../wallace/module_invitationpolicy.py:170 ../wallace/module_optout.py:62 +#: ../wallace/module_resources.py:126 #, python-format msgid "Testing cb_action_%s()" msgstr "" -#: ../wallace/module_footer.py:63 ../wallace/module_optout.py:64 -#: ../wallace/module_resources.py:118 +#: ../wallace/module_footer.py:63 ../wallace/module_gpgencrypt.py:63 +#: ../wallace/module_invitationpolicy.py:172 ../wallace/module_optout.py:64 +#: ../wallace/module_resources.py:129 #, python-format msgid "Attempting to execute cb_action_%s()" msgstr "" @@ -2767,6 +2899,299 @@ msgstr "" msgid "Executing module footer for %r, %r" msgstr "" +#: ../wallace/module_gpgencrypt.py:66 +#, python-format +msgid "Executing module gpgencrypt for %r, %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:98 +msgid "Message is already encrypted (app/pgp-enc content-type)" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:102 +msgid "Message already encrypted by main content-type header" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:131 +msgid "" +"Configured to encrypt to a key not configured, and strict policy enabled. " +"Bailing out." +msgstr "" + +#: ../wallace/module_gpgencrypt.py:134 +msgid "" +"Configured to encrypt to a key not configured, but continuing anyway (see " +"'gpgencrypt_strict')." +msgstr "" + +#: ../wallace/module_gpgencrypt.py:171 +#, python-format +msgid "Recipients: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:183 +#, python-format +msgid "Current keys: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:188 +#, python-format +msgid "Retrieving key for recipient: %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:192 ../wallace/module_gpgencrypt.py:208 +#, python-format +msgid "Found matching address %r" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:200 +#, python-format +msgid "Found matching address %r in remote keys" +msgstr "" + +#: ../wallace/module_gpgencrypt.py:232 +#, python-format +msgid "An error occurred: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:154 +#, python-format +msgid "Invitation policy called for %r, %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:211 +#: ../wallace/module_resources.py:169 +#, python-format +msgid "Failed to parse iTip events from message: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:215 +msgid "" +"Message is not an iTip message or does not contain any (valid) iTip events." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:219 +#, python-format +msgid "" +"iTip events attached to this message contain the following information: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:232 +#, python-format +msgid "No itips, no users, pass along %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:235 +#, python-format +msgid "iTips, but no users, pass along %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:255 +#, python-format +msgid "No user attendee matching envelope recipient %s, skip message" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:259 +#, python-format +msgid "Receiving user: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:284 +#, python-format +msgid "Apply invitation policy %r for domain %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:295 +#, python-format +msgid "Ignoring '%s' iTip method" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:299 +#, python-format +msgid "iTip message %r consumed by the invitationpolicy module" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:315 +msgid "Pass invitation for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:320 +#, python-format +msgid "Receiving Attendee: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:339 +#, python-format +msgid "Existing event: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:350 +#, python-format +msgid "Precondition for event %r fulfilled: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:386 +#, python-format +msgid "No RSVP for recipient %r requested" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:412 +msgid "Pass reply for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:419 +#, python-format +msgid "Sender Attendee: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:431 +#, python-format +msgid "" +"The iTip reply sequence (%r) doesn't match the referred event version (%r). " +"Forwarding to Inbox." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:437 +#, python-format +msgid "Auto-updating event %r on iTip REPLY" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:459 +#: ../wallace/module_invitationpolicy.py:488 +msgid "" +"The event referred by this reply was not found in the user's calendars. " +"Forwarding to Inbox." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:472 +msgid "Pass cancellation for manual processing" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:517 +#, python-format +msgid "Checking if email address %r belongs to a local user" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:522 +#, python-format +msgid "User DN: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:524 +#, python-format +msgid "No user record(s) found for %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:577 +#, python-format +msgid "User record doesn't have the mailbox attribute %r set" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:590 +#, python-format +msgid "IMAP proxy authentication failed: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:612 +#, python-format +msgid "List calendar folders for user %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:628 +#, python-format +msgid "IMAP metadata for %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:658 +#, python-format +msgid "Searching folder %r for event %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:670 +#: ../wallace/module_invitationpolicy.py:709 +#: ../wallace/module_resources.py:486 +#, python-format +msgid "Failed to parse event from message %s/%s: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:696 +#, python-format +msgid "Listing events from folder %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:715 +#, python-format +msgid "Existing event %r conflicts with invitation %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:722 +#: ../wallace/module_resources.py:344 +#, python-format +msgid "start: %r, end: %r, total: %r, messages: %d" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:748 +#, python-format +msgid "%r is locked, waiting..." +msgstr "" + +#: ../wallace/module_invitationpolicy.py:811 +#, python-format +msgid "Failed to save event: no calendar folder found for user %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:814 +#, python-format +msgid "Save event %r to user calendar %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:827 +#, python-format +msgid "Failed to save event to user calendar at %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:843 +#, python-format +msgid "Delete event %r in %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:863 +#, python-format +msgid "Compose participation status summary for event %r to user %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:901 +#, python-format +msgid "" +"Waiting for more automated replies (got %d of %d); skipping notification" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:998 +#, python-format +msgid "Updated %s's copy of %r: %r" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1001 +#, python-format +msgid "Attendee %s's copy of %r not found" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1004 +#, python-format +msgid "Attendee %r not found in LDAP" +msgstr "" + +#: ../wallace/module_invitationpolicy.py:1008 +#, python-format +msgid "" +"\n" +" %(name)s has %(status)s your invitation for %(summary)s.\n" +"\n" +" *** This is an automated response sent by the Kolab Invitation " +"system ***\n" +" " +msgstr "" + #. modules.next_module('optout') #: ../wallace/module_optout.py:70 #, python-format @@ -2788,220 +3213,262 @@ msgstr "" msgid "Could not send request to optout_url %s" msgstr "" -#: ../wallace/module_resources.py:96 +#: ../wallace/module_resources.py:110 #, python-format msgid "Resource Management called for %r, %r" msgstr "" -#: ../wallace/module_resources.py:159 +#: ../wallace/module_resources.py:174 msgid "Message is not an iTip message or does not contain any " msgstr "" -#: ../wallace/module_resources.py:167 +#: ../wallace/module_resources.py:182 msgid "iTip events attached to this message contain the " msgstr "" -#: ../wallace/module_resources.py:188 +#: ../wallace/module_resources.py:205 msgid "Not an iTip message, but sent to resource nonetheless. Reject message" msgstr "" -#: ../wallace/module_resources.py:196 -msgid "No itips, no resources, pass along" +#: ../wallace/module_resources.py:213 +#, python-format +msgid "No itips, no resources, pass along %r" msgstr "" -#: ../wallace/module_resources.py:200 -msgid "iTips, but no resources, pass along" +#: ../wallace/module_resources.py:216 +#, python-format +msgid "iTips, but no resources, pass along %r" +msgstr "" + +#: ../wallace/module_resources.py:225 +#, python-format +msgid "No resource attendees matching envelope recipient %s, Reject message" msgstr "" -#: ../wallace/module_resources.py:233 +#: ../wallace/module_resources.py:234 #, python-format msgid "Resources: %r; %r" msgstr "" -#: ../wallace/module_resources.py:242 +#: ../wallace/module_resources.py:244 #, python-format msgid "Receiving Resource: %r; %r" msgstr "" -#: ../wallace/module_resources.py:250 +#: ../wallace/module_resources.py:252 #, python-format msgid "Recipient %r is non-participant, ignoring message" msgstr "" -#: ../wallace/module_resources.py:281 +#: ../wallace/module_resources.py:279 #, python-format -msgid "Failed to read resource calendar for %r: %r" +msgid "Accept invitation for individual resource %r / %r" msgstr "" -#: ../wallace/module_resources.py:286 +#: ../wallace/module_resources.py:308 #, python-format -msgid "start: %r, end: %r, total: %r, messages: %d" +msgid "Delegate invitation for resource collection %r to %r" +msgstr "" + +#: ../wallace/module_resources.py:340 +#, python-format +msgid "Failed to read resource calendar for %r: %r" msgstr "" -#: ../wallace/module_resources.py:292 +#: ../wallace/module_resources.py:350 #, python-format msgid "Polling for resource %r" msgstr "" -#: ../wallace/module_resources.py:295 +#: ../wallace/module_resources.py:353 #, python-format msgid "Resource %r has been popped from the list" msgstr "" -#: ../wallace/module_resources.py:299 +#: ../wallace/module_resources.py:357 msgid "Resource is a collection" msgstr "" -#: ../wallace/module_resources.py:310 +#: ../wallace/module_resources.py:368 #, python-format msgid "Removed conflicting resources from %r: (%r) => %r" msgstr "" -#: ../wallace/module_resources.py:322 +#: ../wallace/module_resources.py:380 #, python-format msgid "Conflicting events: %r for resource %r" msgstr "" -#: ../wallace/module_resources.py:351 +#: ../wallace/module_resources.py:397 #, python-format -msgid "Accept invitation for individual resource %r / %r" +msgid "Delegate to another resource collection member: %r to %r" msgstr "" -#: ../wallace/module_resources.py:365 -#, python-format -msgid "Delegate invitation for resource collection %r to %r" -msgstr "" - -#: ../wallace/module_resources.py:408 +#: ../wallace/module_resources.py:459 #, python-format msgid "Checking events in resource folder %r" msgstr "" -#: ../wallace/module_resources.py:424 +#: ../wallace/module_resources.py:475 #, python-format msgid "Fetching message UID %r from folder %r" msgstr "" -#: ../wallace/module_resources.py:472 +#: ../wallace/module_resources.py:498 #, python-format msgid "Event %r conflicts with event %r" msgstr "" -#: ../wallace/module_resources.py:499 +#: ../wallace/module_resources.py:525 #, python-format msgid "Adding event to %r: %r" msgstr "" -#: ../wallace/module_resources.py:537 +#: ../wallace/module_resources.py:573 #, python-format msgid "Failed to save event to resource calendar at %r: %r" msgstr "" -#: ../wallace/module_resources.py:553 +#: ../wallace/module_resources.py:590 #, python-format msgid "Delete resource calendar object %r in %r: %r" msgstr "" -#: ../wallace/module_resources.py:587 -#, python-format -msgid "Method %r not really interesting for us." -msgstr "" - -#: ../wallace/module_resources.py:595 -#, python-format -msgid "Raw iTip payload: %s" -msgstr "" - -#: ../wallace/module_resources.py:605 -msgid "Could not read iTip from message." -msgstr "" - -#: ../wallace/module_resources.py:613 -#, python-format -msgid "Duplicate iTip event: %s" -msgstr "" - -#: ../wallace/module_resources.py:638 -msgid "iTip event without a start" -msgstr "" - -#: ../wallace/module_resources.py:676 -msgid "Message is not an iTip message (non-multipart message)" -msgstr "" - -#: ../wallace/module_resources.py:705 +#: ../wallace/module_resources.py:633 #, python-format msgid "Checking if email address %r belongs to a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:713 ../wallace/module_resources.py:781 -#: ../wallace/module_resources.py:815 +#: ../wallace/module_resources.py:641 ../wallace/module_resources.py:709 +#: ../wallace/module_resources.py:743 #, python-format msgid "Resource record(s): %r" msgstr "" -#: ../wallace/module_resources.py:715 ../wallace/module_resources.py:783 -#: ../wallace/module_resources.py:818 +#: ../wallace/module_resources.py:643 ../wallace/module_resources.py:711 +#: ../wallace/module_resources.py:746 #, python-format msgid "No resource (collection) records found for %r" msgstr "" -#: ../wallace/module_resources.py:719 ../wallace/module_resources.py:787 -#: ../wallace/module_resources.py:822 +#: ../wallace/module_resources.py:647 ../wallace/module_resources.py:715 +#: ../wallace/module_resources.py:750 #, python-format msgid "Resource record: %r" msgstr "" -#: ../wallace/module_resources.py:739 +#: ../wallace/module_resources.py:667 #, python-format msgid "Raw itip_events: %r" msgstr "" -#: ../wallace/module_resources.py:747 +#: ../wallace/module_resources.py:675 #, python-format msgid "Raw set of attendees: %r" msgstr "" -#: ../wallace/module_resources.py:755 +#: ../wallace/module_resources.py:683 #, python-format msgid "Raw set of resources: %r" msgstr "" -#: ../wallace/module_resources.py:774 +#: ../wallace/module_resources.py:702 #, python-format msgid "Checking if attendee %r is a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:790 ../wallace/module_resources.py:824 +#: ../wallace/module_resources.py:718 ../wallace/module_resources.py:752 msgid "Resource reservation made but no resource records found" msgstr "" -#: ../wallace/module_resources.py:809 +#: ../wallace/module_resources.py:737 #, python-format msgid "Checking if resource %r is a resource (collection)" msgstr "" -#: ../wallace/module_resources.py:827 +#: ../wallace/module_resources.py:755 msgid "The following resources are being referred to in the " msgstr "" -#: ../wallace/module_resources.py:867 +#: ../wallace/module_resources.py:894 #, python-format msgid "" "\n" -" Your reservation was delegated to \"%s\"\n" -" which is available for the requested time.\n" +" *** This is an automated response, please do not reply! ***\n" +"\n" +" Your reservation was delegated to \"%s\" which is available " +"for the requested time.\n" " " msgstr "" +#: ../wallace/module_resources.py:905 +#, python-format +msgid "" +"\n" +" *** This is an automated response, please do not reply! ***\n" +" \n" +" We hereby inform you that your reservation was %s.\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:912 +#, python-format +msgid "" +"\n" +" If you have questions about this reservation, please contact\n" +" %s <%s> %s\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:941 +#, python-format +msgid "Sending booking notification for event %r to %r from %r" +msgstr "" + +#: ../wallace/module_resources.py:954 +msgid "failed" +msgstr "" + +#: ../wallace/module_resources.py:973 +#, python-format +msgid "" +"\n" +" The resource booking for %(resource)s by %(orgname)s <%(orgemail)" +"s> has been %(status)s for %(date)s.\n" +"\n" +" *** This is an automated message, sent to you as the resource " +"owner. ***\n" +" " +msgstr "" + +#: ../wallace/module_resources.py:979 +#, python-format +msgid "" +"\n" +" A reservation request for %(resource)s could not be processed " +"automatically.\n" +" Please contact %(orgname)s <%(orgemail)s> who requested this " +"resource for %(date)s. Subject: %(summary)s.\n" +"\n" +" *** This is an automated message, sent to you as the resource " +"owner. ***\n" +" " +msgstr "" + #. This is a nested module #: ../wallace/modules.py:97 #, python-format msgid "Module Group: %s" msgstr "" -#: ../wallace/modules.py:108 ../wallace/modules.py:113 -msgid "No such module." +#: ../wallace/modules.py:108 +#, python-format +msgid "No such module %r in modules %r (1)." +msgstr "" + +#: ../wallace/modules.py:113 +#, python-format +msgid "No such module %r in modules %r (2)." msgstr "" #: ../wallace/modules.py:119 diff --git a/pykolab/Makefile.am b/pykolab/Makefile.am index cb127cf..e43cdbb 100644 --- a/pykolab/Makefile.am +++ b/pykolab/Makefile.am @@ -42,6 +42,10 @@ pykolab_imap_PYTHON = \ imap/__init__.py \ imap/cyrus.py +pykolab_itipdir = $(pythondir)/$(PACKAGE)/itip +pykolab_itip_PYTHON = \ + itip/__init__.py + pykolab_pluginsdir = $(pythondir)/$(PACKAGE)/plugins pykolab_plugins_PYTHON = \ plugins/__init__.py diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py index 32d1c26..61e848b 100644 --- a/pykolab/auth/__init__.py +++ b/pykolab/auth/__init__.py @@ -204,7 +204,10 @@ class Auth(pykolab.base.Base): return result def find_user(self, attr, value, **kw): - return self._auth._find_user(attr, value, **kw) + return self._auth.search_entry_by_attribute(attr, value, **kw) + + def find_user_dn(self, login, kolabuser=False): + return self._auth._find_user_dn(login, kolabuser); def list_domains(self, domain=None): """ diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py index d36f7f6..5fd337e 100644 --- a/pykolab/auth/ldap/__init__.py +++ b/pykolab/auth/ldap/__init__.py @@ -416,7 +416,7 @@ class LDAP(pykolab.base.Base): if entry[folderacl_entry_attribute] is not None: # Parse it before assigning it - entry['kolabmailfolderaclentry'] = [] + entry['kolabfolderaclentry'] = [] if not isinstance(entry[folderacl_entry_attribute], list): entry[folderacl_entry_attribute] = [ entry[folderacl_entry_attribute] ] @@ -427,11 +427,15 @@ class LDAP(pykolab.base.Base): log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8) access_lookup_dict = { + 'all': 'lrsedntxakcpiw', + 'append': 'wip', + 'full': 'lrswipkxtecdn', 'read': 'lrs', + 'read-only': 'lrs', + 'read-write': 'lrswitedn', 'post': 'p', - 'append': 'wip', + 'semi-full': 'lrswit', 'write': 'lrswite', - 'all': 'lrsedntxakcpiw' } if access_lookup_dict.has_key(acl_access): @@ -439,9 +443,9 @@ class LDAP(pykolab.base.Base): log.debug(_("Found a subject %r with access %r") % (aci_subject, acl_access), level=8) - entry['kolabmailfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access)) + entry['kolabfolderaclentry'].append("(%r, %r, %r)" % (folder_path, aci_subject, acl_access)) - self.init_entry_attribute(entry, 'kolabmailfolderaclentry') + self.init_entry_attribute(entry, 'kolabfolderaclentry') def get_folder_path(self, entry): """ @@ -1187,7 +1191,7 @@ class LDAP(pykolab.base.Base): """ pass - def _change_add_sharedfolder(self, entry, change): + def _change_add_sharedfolder(self, entry, change, modify=False): """ An entry of type sharedfolder was added. """ @@ -1235,11 +1239,15 @@ class LDAP(pykolab.base.Base): folder_path, entry['kolabfoldertype'] ) + elif modify: + self.imap.set_acl(folder_path, 'anyone', '') - if entry.get('kolabmailfolderaclentry'): + if entry.get('kolabfolderaclentry'): self.imap._set_kolab_mailfolder_acls( - entry['kolabmailfolderaclentry'] + entry['kolabfolderaclentry'] ) + else: + self.imap.set_acl(folder_path, 'anyone', '') if entry.get(delivery_address_attribute): self.imap.set_acl(folder_path, 'anyone', '+p') @@ -1374,7 +1382,7 @@ class LDAP(pykolab.base.Base): success = True for _type in ['user','group','role','sharedfolder']: try: - eval("self._change_delete_%s(entry, change)" % (_type)) + eval("success = self._change_delete_%s(entry, change)" % (_type)) except: success = False @@ -1467,8 +1475,8 @@ class LDAP(pykolab.base.Base): def _change_modify_role(self, entry, change): pass - # A shared folder was modified. - _change_modify_sharedfolder = _change_add_sharedfolder + def _change_modify_sharedfolder(self, entry, change): + self._change_add_sharedfolder(entry, change, modify=True) def _change_modify_user(self, entry, change): """ @@ -1575,7 +1583,18 @@ class LDAP(pykolab.base.Base): self.init_entry_attribute(entry, 'kolabfoldertype') - #self.init_entry_attribute(entry, 'kolabmailfolderaclentry') + folderacl_entry_attribute = conf.get('ldap', 'folderacl_entry_attribute') + if folderacl_entry_attribute is None: + folderacl_entry_attribute = 'acl' + + if not entry.has_key(folderacl_entry_attribute): + entry['kolabfolderaclentry'] = self.get_entry_attribute( + entry['id'], + folderacl_entry_attribute + ) + else: + entry['kolabfolderaclentry'] = entry[folderacl_entry_attribute] + del entry[folderacl_entry_attribute] folder_path = self.get_folder_path(entry) @@ -1588,11 +1607,33 @@ class LDAP(pykolab.base.Base): entry['kolabfoldertype'] ) - if entry.get('kolabmailfolderaclentry'): + if entry.get('kolabfolderaclentry'): + + if isinstance(entry['kolabfolderaclentry'], basestring): + entry['kolabfolderaclentry'] = [ entry['kolabfolderaclentry'] ] + + import copy + _acls = copy.deepcopy(entry['kolabfolderaclentry']) + entry['kolabfolderaclentry'] = [] + + for _entry in _acls: + if _entry[0] == "(": + entry['kolabfolderaclentry'].append(_entry) + continue + + s,r = [x.strip() for x in _entry.split(',')] + + entry['kolabfolderaclentry'].append("('%s', '%s', '%s')" % (folder_path, s, r)) + self.imap._set_kolab_mailfolder_acls( - entry['kolabmailfolderaclentry'] + entry['kolabfolderaclentry'] ) + elif entry['kolabfolderaclentry'] in [None,[]]: + for ace in self.imap.list_acls(folder_path): + aci_subject = ace.split()[0] + self.imap.set_acl(folder_path, aci_subject, '') + delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute') if entry.get(delivery_address_attribute): self.imap.set_acl(folder_path, 'anyone', '+p') @@ -1766,16 +1807,17 @@ class LDAP(pykolab.base.Base): else: return _type - def _find_user_dn(self, login, realm): + def _find_user_dn(self, login, kolabuser=False): """ - Find the distinguished name (DN) for an entry in LDAP. + Find the distinguished name (DN) for a (Kolab) user entry in LDAP. """ + conf_prefix = 'kolab_' if kolabuser else '' domain_root_dn = self._kolab_domain_root_dn(self.domain) - base_dn = self.config_get('user_base_dn') - if base_dn is None: - base_dn = self.config_get('base_dn') + user_base_dn = self.config_get(conf_prefix + 'user_base_dn') + if user_base_dn is None: + user_base_dn = self.config_get('base_dn') auth_attrs = self.config_get_list('auth_attributes') @@ -1783,18 +1825,21 @@ class LDAP(pykolab.base.Base): for auth_attr in auth_attrs: auth_search_filter.append('(%s=%s)' % (auth_attr,login)) - auth_search_filter.append( - '(%s=%s@%s)' % ( - auth_attr, - login, - self.domain - ) - ) + if not '@' in login: + auth_search_filter.append( + '(%s=%s@%s)' % ( + auth_attr, + login, + self.domain + ) + ) auth_search_filter.append(')') auth_search_filter = ''.join(auth_search_filter) + user_filter = self.config_get(conf_prefix + 'user_filter') + search_filter = "(&%s%s)" % ( auth_search_filter, user_filter @@ -2018,7 +2063,7 @@ class LDAP(pykolab.base.Base): try: entry['type'] = self._entry_type(entry) except: - entry['type'] = "unknown" + entry['type'] = None log.debug(_("Entry type: %s") % (entry['type']), level=8) @@ -2092,14 +2137,6 @@ class LDAP(pykolab.base.Base): # # server = self.imap.user_mailbox_server(folder) - log.debug( - _("Done with _synchronize_callback() for entry %r") % ( - entry['id'] - ), - level=9 - ) - - def _unbind(self): """ Discard the current set of bind credentials. @@ -2468,9 +2505,13 @@ class LDAP(pykolab.base.Base): except Exception, errmsg: log.error(_("An error occured using %s: %r") % (supported_control, errmsg)) + import traceback + if conf.debuglevel > 8: - import traceback traceback.print_exc() + + log.error(_("%s") % (traceback.format_exc())) + continue return _results diff --git a/pykolab/auth/ldap/cache.py b/pykolab/auth/ldap/cache.py index 292bc52..5dcbb76 100644 --- a/pykolab/auth/ldap/cache.py +++ b/pykolab/auth/ldap/cache.py @@ -54,7 +54,7 @@ class Entry(object): self.uniqueid = uniqueid self.result_attribute = result_attr - modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format') + modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format') if modifytimestamp_format is None: modifytimestamp_format = "%Y%m%d%H%M%SZ" @@ -86,7 +86,7 @@ mapper(Entry, entry_table) ## def delete_entry(domain, entry): - result_attribute = conf.get('cyrus-sasl', 'result_attribute') + result_attribute = conf.get_raw('cyrus-sasl', 'result_attribute') db = init_db(domain) _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first() @@ -96,7 +96,7 @@ def delete_entry(domain, entry): db.commit() def get_entry(domain, entry, update=True): - result_attribute = conf.get('cyrus-sasl', 'result_attribute') + result_attribute = conf.get_raw('cyrus-sasl', 'result_attribute') _entry = None @@ -130,7 +130,7 @@ def get_entry(domain, entry, update=True): db.commit() _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first() else: - modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format') + modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format') if modifytimestamp_format is None: modifytimestamp_format = "%Y%m%d%H%M%SZ" @@ -166,9 +166,13 @@ def init_db(domain,reinit=False): db_uri = 'sqlite:///%s/%s.db' % (KOLAB_LIB_PATH, domain) echo = conf.debuglevel > 8 - engine = create_engine(db_uri, echo=echo) - metadata.create_all(engine) + try: + engine = create_engine(db_uri, echo=echo) + metadata.create_all(engine) + except: + engine = create_engine('sqlite://') + metadata.create_all(engine) Session = sessionmaker(bind=engine) db = Session() @@ -176,14 +180,17 @@ def init_db(domain,reinit=False): return db def last_modify_timestamp(domain): - db = init_db(domain) - last_change = db.query(Entry).order_by(desc(Entry.last_change)).first() - - modifytimestamp_format = conf.get('ldap', 'modifytimestamp_format') + modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format') if modifytimestamp_format is None: modifytimestamp_format = "%Y%m%d%H%M%SZ" - if last_change is not None: - return last_change.last_change.strftime(modifytimestamp_format) - - return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format) + try: + db = init_db(domain) + last_change = db.query(Entry).order_by(desc(Entry.last_change)).first() + + if last_change is not None: + return last_change.last_change.strftime(modifytimestamp_format) + else: + return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format) + except: + return datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format) diff --git a/pykolab/cli/cmd_count_domain_mailboxes.py b/pykolab/cli/cmd_count_domain_mailboxes.py index 8aecd2d..958bccd 100644 --- a/pykolab/cli/cmd_count_domain_mailboxes.py +++ b/pykolab/cli/cmd_count_domain_mailboxes.py @@ -56,10 +56,8 @@ def execute(*args, **kw): domains = auth.list_domains() folders = [] - for primary,secondaries in domains: - print "%s: %d" % (primary,len(imap.lm("user/%%@%s" % (primary)))) - for secondary in secondaries: - print "%s: %d" % (secondary,len(imap.lm("user/%%@%s" % (secondary)))) + for domain in domains.keys(): + print "%s: %d" % (domain,len(imap.lm("user/%%@%s" % (domain)))) null_realm = len(imap.lm("user/%%")) diff --git a/pykolab/cli/cmd_delete_mailbox.py b/pykolab/cli/cmd_delete_mailbox.py index 3f9c981..5a65f56 100644 --- a/pykolab/cli/cmd_delete_mailbox.py +++ b/pykolab/cli/cmd_delete_mailbox.py @@ -58,5 +58,8 @@ def execute(*args, **kw): sys.exit(1) for delete_folder in delete_folders: - imap.delete_mailfolder(delete_folder) + try: + imap.delete_mailfolder(delete_folder) + except Exception, errmsg: + log.error(_("Could not delete mailbox '%s'") % (delete_folder)) diff --git a/pykolab/cli/cmd_list_ous.py b/pykolab/cli/cmd_list_ous.py new file mode 100644 index 0000000..670b609 --- /dev/null +++ b/pykolab/cli/cmd_list_ous.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com) +# +# Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen a kolabsys.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import commands + +import pykolab + +from pykolab import utils +from pykolab.translate import _ + +log = pykolab.getLogger('pykolab.cli') +conf = pykolab.getConf() + +def __init__(): + commands.register('list_ous', execute, description="List organizational units.") + +def execute(*args, **kw): + from pykolab import wap_client + + wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw")) + + ous = wap_client.ous_list() + print '\n'.join(ous['list'].keys()) diff --git a/pykolab/cli/cmd_list_users.py b/pykolab/cli/cmd_list_users.py new file mode 100644 index 0000000..ff1ddef --- /dev/null +++ b/pykolab/cli/cmd_list_users.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com) +# +# Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen a kolabsys.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import commands + +import pykolab + +from pykolab import utils +from pykolab.translate import _ + +log = pykolab.getLogger('pykolab.cli') +conf = pykolab.getConf() + +def __init__(): + commands.register('list_users', execute, description="List organizational units.") + +def execute(*args, **kw): + from pykolab import wap_client + + wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw")) + + users = wap_client.users_list() + print '\n'.join(users['list'].keys()) diff --git a/pykolab/cli/cmd_rename_mailbox.py b/pykolab/cli/cmd_rename_mailbox.py index f7cf1c1..dfb759d 100644 --- a/pykolab/cli/cmd_rename_mailbox.py +++ b/pykolab/cli/cmd_rename_mailbox.py @@ -66,7 +66,7 @@ def execute(*args, **kw): utils.message(_("Source folder %r does not exist") % (source_folder)) sys.exit(1) - if imap.has_folder(target_folder): + if imap.has_folder(target_folder) and partition is None: utils.message(_("Target folder %r already exists") % (target_folder)) sys.exit(1) diff --git a/pykolab/cli/commands.py b/pykolab/cli/commands.py index 82241a4..b6dff3a 100644 --- a/pykolab/cli/commands.py +++ b/pykolab/cli/commands.py @@ -35,7 +35,6 @@ def __init__(): register('help', lambda *args, **kw: list_commands(commands, *args, **kw)) - register('list_users', not_yet_implemented, description="Not yet implemented") register('delete_user', not_yet_implemented, description="Not yet implemented") register('list_groups', not_yet_implemented, description="Not yet implemented") diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py index 2115bdf..be71fd5 100644 --- a/pykolab/imap/__init__.py +++ b/pykolab/imap/__init__.py @@ -211,8 +211,7 @@ class IMAP(object): folder_path = self.folder_utf7(folder_path) if server is not None: - if not self._imap.has_key(server): - self.connect(server=server) + self.connect(server=server) try: self._imap[server].cm(folder_path, partition=partition) @@ -321,11 +320,15 @@ class IMAP(object): Set an ACL entry on a folder. """ short_rights = { - 'all': 'lrswipkxtecda', + 'all': 'lrsedntxakcpiw', + 'append': 'wip', + 'full': 'lrswipkxtecdn', + 'read': 'lrs', 'read-only': 'lrs', - 'read-write': 'lrswited', + 'read-write': 'lrswitedn', + 'post': 'p', 'semi-full': 'lrswit', - 'full': 'lrswipkxtecd' + 'write': 'lrswite', } if short_rights.has_key(acl): @@ -651,7 +654,7 @@ class IMAP(object): if additional_folders[additional_folder].has_key("partition"): partition = additional_folders[additional_folder]["partition"] try: - self.imap.rename(folder_name, folder_name, partition) + self.imap._rename(folder_name, folder_name, partition) except: log.error(_("Could not rename %s to reside on partition %s") % (folder_name, partition)) diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py index 978fef9..9664edd 100644 --- a/pykolab/imap/cyrus.py +++ b/pykolab/imap/cyrus.py @@ -236,8 +236,12 @@ class Cyrus(cyruslib.CYRUS): server = self.find_mailfolder_server(from_mailfolder) self.connect(self.uri.replace(self.server,server)) - log.debug(_("Moving INBOX folder %s to %s") % (from_mailfolder,to_mailfolder), level=8) - self.m.rename(from_mailfolder, to_mailfolder, partition) + if not partition == None: + log.debug(_("Moving INBOX folder %s to %s on partition %s") % (from_mailfolder,to_mailfolder, partition), level=8) + else: + log.debug(_("Moving INBOX folder %s to %s") % (from_mailfolder,to_mailfolder), level=8) + + self.m.rename(self.folder_utf7(from_mailfolder), self.folder_utf7(to_mailfolder), '"%s"' % (partition)) def _getannotation(self, *args, **kw): return self.getannotation(*args, **kw) @@ -384,9 +388,9 @@ class Cyrus(cyruslib.CYRUS): verify_folder_search = "%s@%s" % (verify_folder_search, mbox['domain']) if ' ' in verify_folder_search: - folders = self.lm('"%s"' % verify_folder_search) + folders = self.lm('"%s"' % self.folder_utf7(verify_folder_search)) else: - folders = self.lm(verify_folder_search) + folders = self.lm(self.folder_utf7(verify_folder_search)) # NOTE: Case also covered is valid hexadecimal folders; won't be the # actual check as intended, but doesn't give you anyone else's data diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py new file mode 100644 index 0000000..ddcb392 --- /dev/null +++ b/pykolab/itip/__init__.py @@ -0,0 +1,287 @@ +import icalendar +import pykolab + +from pykolab.xml import to_dt +from pykolab.xml import event_from_ical +from pykolab.xml import participant_status_label +from pykolab.translate import _ + +log = pykolab.getLogger('pykolab.wallace') + + +def events_from_message(message, methods=None): + return objects_from_message(message, "VEVENT", methods) + +def todos_from_message(message, methods=None): + return objects_from_message(message, "VTODO", methods) + + +def objects_from_message(message, objname, methods=None): + """ + Obtain the iTip payload from email.message <message> + """ + # Placeholder for any itip_objects found in the message. + itip_objects = [] + seen_uids = [] + + # iTip methods we are actually interested in. Other methods will be ignored. + if methods is None: + methods = [ "REQUEST", "CANCEL" ] + + # Are all iTip messages multipart? No! RFC 6047, section 2.4 states "A + # MIME body part containing content information that conforms to this + # document MUST have (...)" but does not state whether an iTip message must + # therefore also be multipart. + + # Check each part + for part in message.walk(): + + # The iTip part MUST be Content-Type: text/calendar (RFC 6047, section 2.4) + # But in real word, other mime-types are used as well + if part.get_content_type() in [ "text/calendar", "text/x-vcalendar", "application/ics" ]: + if not str(part.get_param('method')).upper() in methods: + log.info(_("Method %r not really interesting for us.") % (part.get_param('method'))) + continue + + # Get the itip_payload + itip_payload = part.get_payload(decode=True) + + log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9) + + # Python iCalendar prior to 3.0 uses "from_string". + if hasattr(icalendar.Calendar, 'from_ical'): + cal = icalendar.Calendar.from_ical(itip_payload) + elif hasattr(icalendar.Calendar, 'from_string'): + cal = icalendar.Calendar.from_string(itip_payload) + + # If we can't read it, we're out + else: + log.error(_("Could not read iTip from message.")) + return [] + + for c in cal.walk(): + if c.name == objname: + itip = {} + + if c['uid'] in seen_uids: + log.debug(_("Duplicate iTip object: %s") % (c['uid']), level=9) + continue + + # From the event, take the following properties: + # + # - method + # - uid + # - sequence + # - start + # - end (if any) + # - duration (if any) + # - organizer + # - attendees (if any) + # - resources (if any) + # + + itip['uid'] = str(c['uid']) + itip['method'] = str(cal['method']).upper() + itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0 + + if c.has_key('dtstart'): + itip['start'] = c['dtstart'].dt + else: + log.error(_("iTip event without a start")) + continue + + if c.has_key('dtend'): + itip['end'] = c['dtend'].dt + + if c.has_key('duration'): + itip['duration'] = c['duration'].dt + itip['end'] = itip['start'] + c['duration'].dt + + itip['organizer'] = c['organizer'] + + itip['attendees'] = c['attendee'] + + if itip.has_key('attendees') and not isinstance(itip['attendees'], list): + itip['attendees'] = [c['attendee']] + + if c.has_key('resources'): + itip['resources'] = c['resources'] + + itip['raw'] = itip_payload + + try: + # TODO: distinguish event and todo here + itip['xml'] = event_from_ical(c.to_ical()) + except Exception, e: + log.error("event_from_ical() exception: %r; iCal: %s" % (e, itip_payload)) + continue + + itip_objects.append(itip) + + seen_uids.append(c['uid']) + + # end if c.name == "VEVENT" + + # end for c in cal.walk() + + # end if part.get_content_type() == "text/calendar" + + # end for part in message.walk() + + if not len(itip_objects) and not message.is_multipart(): + log.debug(_("Message is not an iTip message (non-multipart message)"), level=5) + + return itip_objects + + +def check_event_conflict(kolab_event, itip_event): + """ + Determine whether the given kolab event conflicts with the given itip event + """ + conflict = False + + # don't consider conflict with myself + if kolab_event.uid == itip_event['uid']: + return conflict + + # don't consider conflict if event has TRANSP:TRANSPARENT + if kolab_event.get_transparency(): + return conflict + + _es = to_dt(kolab_event.get_start()) + _ee = to_dt(kolab_event.get_ical_dtend()) # use iCal style end date: next day for all-day events + + # naive loops to check for collisions in (recurring) events + # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday) + while not conflict and _es is not None: + _is = to_dt(itip_event['start']) + _ie = to_dt(itip_event['end']) + + while not conflict and _is is not None: + # log.debug("* Comparing event dates at %s/%s with %s/%s" % (_es, _ee, _is, _ie), level=9) + conflict = check_date_conflict(_es, _ee, _is, _ie) + _is = to_dt(itip_event['xml'].get_next_occurence(_is)) if kolab_event.is_recurring() else None + _ie = to_dt(itip_event['xml'].get_occurence_end_date(_is)) + + _es = to_dt(kolab_event.get_next_occurence(_es)) if kolab_event.is_recurring() else None + _ee = to_dt(kolab_event.get_occurence_end_date(_es)) + + return conflict + + +def check_date_conflict(_es, _ee, _is, _ie): + """ + Check the given event start/end dates for conflicts + """ + conflict = False + + # TODO: add margin for all-day dates (+13h; -12h) + + if _es < _is: + if _es <= _ie: + if _ee <= _is: + conflict = False + else: + conflict = True + else: + conflict = True + elif _es == _is: + conflict = True + else: # _es > _is + if _es <= _ie: + conflict = True + else: + conflict = False + + return conflict + + +def send_reply(from_address, itip_events, response_text, subject=None): + """ + Send the given iCal events as a valid iTip REPLY to the organizer. + """ + import smtplib + + conf = pykolab.getConf() + smtp = None + + if isinstance(itip_events, dict): + itip_events = [ itip_events ] + + for itip_event in itip_events: + attendee = itip_event['xml'].get_attendee_by_email(from_address) + participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee) + + event_summary = itip_event['xml'].get_summary() + message_text = response_text % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() } + + if subject is not None: + subject = subject % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() } + + try: + message = itip_event['xml'].to_message_itip(from_address, + method="REPLY", + participant_status=participant_status, + message_text=message_text, + subject=subject + ) + except Exception, e: + log.error(_("Failed to compose iTip reply message: %r") % (e)) + return + + smtp = smtplib.SMTP("localhost", 10026) # replies go through wallace again + + if conf.debuglevel > 8: + smtp.set_debuglevel(True) + + try: + smtp.sendmail(message['From'], message['To'], message.as_string()) + except Exception, e: + log.error(_("SMTP sendmail error: %r") % (e)) + + if smtp: + smtp.quit() + + +def send_request(to_address, itip_events, request_text, subject=None, direct=False): + """ + Send an iTip REQUEST message from the given iCal events + """ + import smtplib + + conf = pykolab.getConf() + smtp = None + + if isinstance(itip_events, dict): + itip_events = [ itip_events ] + + for itip_event in itip_events: + event_summary = itip_event['xml'].get_summary() + message_text = request_text % { 'summary':event_summary } + + if subject is not None: + subject = subject % { 'summary':event_summary } + + try: + message = itip_event['xml'].to_message_itip(None, + method="REQUEST", + message_text=message_text, + subject=subject + ) + except Exception, e: + log.error(_("Failed to compose iTip request message: %r") % (e)) + return + + port = 10027 if direct else 10026 + smtp = smtplib.SMTP("localhost", port) + + if conf.debuglevel > 8: + smtp.set_debuglevel(True) + + try: + smtp.sendmail(message['From'], to_address, message.as_string()) + except Exception, e: + log.error(_("SMTP sendmail error: %r") % (e)) + + if smtp: + smtp.quit() diff --git a/pykolab/translate.py b/pykolab/translate.py index bee8fc2..080cbc2 100644 --- a/pykolab/translate.py +++ b/pykolab/translate.py @@ -26,7 +26,10 @@ import gettext import os N_ = lambda x: x -_ = lambda x: gettext.ldgettext(domain, x) +_ = lambda x: current.lgettext(x) + +localedir = '/usr/local/share/locale' +current = gettext.translation(domain, localedir, fallback=True) def getDefaultLangs(): languages = [] @@ -45,3 +48,16 @@ def getDefaultLangs(): if nelang not in nelangs: nelangs.append(nelang) return nelangs + +def setUserLanguage(lang): + global current + + langs = [] + for l in gettext._expand_lang(lang): + if l not in langs: + langs.append(l) + + try: + current = gettext.translation(domain, localedir, languages=langs, fallback=True) + except: + pass diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py index 38a5381..7b2b565 100644 --- a/pykolab/wap_client/__init__.py +++ b/pykolab/wap_client/__init__.py @@ -67,8 +67,12 @@ def authenticate(username=None, password=None, domain=None): response = request('POST', "system.authenticate", post=post) + if not response: + return False + if response.has_key('session_token'): session_id = response['session_token'] + return True def connect(): global conn @@ -125,6 +129,31 @@ def form_value_generate(params): return request('POST', 'form_value.generate', post=post) +def form_value_generate_password(*args, **kw): + return request('GET', 'form_value.generate_password') + +def form_value_list_options(object_type, object_type_id, attribute): + post = json.dumps( + { + 'object_type': object_type, + 'type_id': object_type_id, + 'attribute': attribute + } + ) + + return request('POST', 'form_value.list_options', post=post) + +def form_value_select_options(object_type, object_type_id, attribute): + post = json.dumps( + { + 'object_type': object_type, + 'type_id': object_type_id, + 'attributes': [ attribute ] + } + ) + + return request('POST', 'form_value.select_options', post=post) + def get_group_input(): group_types = group_types_list() @@ -274,10 +303,18 @@ def group_form_value_generate_mail(params=None): return request('POST', 'group_form_value.generate_mail', params) -def group_info(): - group = utils.ask_question("Group email address") - group = request('GET', 'group.info?group=%s' % (group)) - return group +def group_find(params=None): + post = { 'search': { 'params': {} } } + + for (k,v) in params.iteritems(): + post['search']['params'][k] = { 'value': v, 'type': 'exact' } + + return request('POST', 'group.find', post=json.dumps(post)) + +def group_info(group=None): + if group == None: + group = utils.ask_question("group DN") + return request('GET', 'group.info', get={ 'id': group }) def group_members_list(group=None): if group is None: @@ -288,8 +325,32 @@ def group_members_list(group=None): def group_types_list(): return request('GET', 'group_types.list') -def groups_list(): - return request('GET', 'groups.list') +def groups_list(params={}): + return request('POST', 'groups.list', post=json.dumps(params)) + +def ou_add(params={}): + return request('POST', 'ou.add', post=json.dumps(params)) + +def ou_delete(params={}): + return request('POST', 'ou.delete', post=json.dumps(params)) + +def ou_find(params=None): + post = { 'search': { 'params': {} } } + + for (k,v) in params.iteritems(): + post['search']['params'][k] = { 'value': v, 'type': 'exact' } + + return request('POST', 'ou.find', post=json.dumps(post)) + +def ou_info(ou): + _params = { 'id': ou } + + ou = request('GET', 'ou.info', get=_params) + + return ou + +def ous_list(params={}): + return request('POST', 'ous.list', post=json.dumps(params)) def request(method, api_uri, get=None, post=None, headers={}): response_data = request_raw(method, api_uri, get, post, headers) @@ -298,7 +359,6 @@ def request(method, api_uri, get=None, post=None, headers={}): del response_data['status'] return response_data['result'] else: - print "ERROR: %r" % (response_data['reason']) return False def request_raw(method, api_uri, get=None, post=None, headers={}): @@ -312,8 +372,6 @@ def request_raw(method, api_uri, get=None, post=None, headers={}): if conf.debuglevel > 8: conn.set_debuglevel(9) - conn.set_debuglevel(9) - if get is not None: _get = "?%s" % (urllib.urlencode(get)) else: @@ -337,9 +395,111 @@ def request_raw(method, api_uri, get=None, post=None, headers={}): return response_data +def resource_add(params=None): + if params == None: + params = get_user_input() + + return request('POST', 'resource.add', post=json.dumps(params)) + +def resource_delete(params=None): + if params == None: + params = { + 'id': utils.ask_question("Resource DN to delete", "resource") + } + + return request('POST', 'resource.delete', post=json.dumps(params)) + +def resource_find(params=None): + post = { 'search': { 'params': {} } } + + for (k,v) in params.iteritems(): + post['search']['params'][k] = { 'value': v, 'type': 'exact' } + + return request('POST', 'resource.find', post=json.dumps(post)) + +def resource_info(resource=None): + if resource == None: + resource = utils.ask_question("Resource DN") + return request('GET', 'resource.info', get={ 'id': resource }) + +def resource_types_list(): + return request('GET', 'resource_types.list') + +def resources_list(params={}): + return request('POST', 'resources.list', post=json.dumps(params)) + +def role_add(params=None): + if params is None: + role_name = utils.ask_question("Role name") + params = { + 'cn': role_name + } + + params = json.dumps(params) + + return request('POST', 'role.add', params) + def role_capabilities(): return request('GET', 'role.capabilities') +def role_delete(params=None): + if params is None: + role_name = utils.ask_question("Role name") + role = role_find_by_attribute({'cn': role_name}) + params = { + 'role': role.keys()[0] + } + + if not params.has_key('role'): + role = role_find_by_attribute(params) + params = { + 'role': role.keys()[0] + } + + post = json.dumps(params) + + return request('POST', 'role.delete', post=post) + +def role_find_by_attribute(params=None): + if params is None: + role_name = utils.ask_question("Role name") + else: + role_name = params['cn'] + + get = { 'cn': role_name } + role = request('GET', 'role.find_by_attribute', get=get) + + return role + +def role_info(role_name): + role = role_find_by_attribute({'cn': role_name}) + + get = { 'role': role['id'] } + + role = request('GET', 'role.info', get=get) + + return role + +def roles_list(): + return request('GET', 'roles.list') + +def sharedfolder_add(params=None): + if params == None: + params = get_user_input() + + return request('POST', 'sharedfolder.add', post=json.dumps(params)) + +def sharedfolder_delete(params=None): + if params == None: + params = { + 'id': utils.ask_question("Shared Folder DN to delete", "sharedfolder") + } + + return request('POST', 'sharedfolder.delete', post=json.dumps(params)) + +def sharedfolders_list(params={}): + return request('POST', 'sharedfolders.list', post=json.dumps(params)) + def system_capabilities(): return request('GET', 'system.capabilities') @@ -425,83 +585,6 @@ def user_form_value_generate(params=None): return request('POST', 'form_value.generate', post=post) -def form_value_generate_password(*args, **kw): - return request('GET', 'form_value.generate_password') - -def form_value_list_options(object_type, object_type_id, attribute): - post = json.dumps( - { - 'object_type': object_type, - 'type_id': object_type_id, - 'attribute': attribute - } - ) - - return request('POST', 'form_value.list_options', post=post) - -def form_value_select_options(object_type, object_type_id, attribute): - post = json.dumps( - { - 'object_type': object_type, - 'type_id': object_type_id, - 'attributes': [ attribute ] - } - ) - - return request('POST', 'form_value.select_options', post=post) - -def role_find_by_attribute(params=None): - if params is None: - role_name = utils.ask_question("Role name") - else: - role_name = params['cn'] - - get = { 'cn': role_name } - role = request('GET', 'role.find_by_attribute', get=get) - - return role - -def role_add(params=None): - if params is None: - role_name = utils.ask_question("Role name") - params = { - 'cn': role_name - } - - params = json.dumps(params) - - return request('POST', 'role.add', params) - -def role_delete(params=None): - if params is None: - role_name = utils.ask_question("Role name") - role = role_find_by_attribute({'cn': role_name}) - params = { - 'role': role.keys()[0] - } - - if not params.has_key('role'): - role = role_find_by_attribute(params) - params = { - 'role': role.keys()[0] - } - - post = json.dumps(params) - - return request('POST', 'role.delete', post=post) - -def role_info(role_name): - role = role_find_by_attribute({'cn': role_name}) - - get = { 'role': role['id'] } - - role = request('GET', 'role.info', get=get) - - return role - -def roles_list(): - return request('GET', 'roles.list') - def user_form_value_generate_uid(params=None): if params is None: params = get_user_input() @@ -524,34 +607,8 @@ def user_info(user=None): return user +def users_list(params={}): + return request('POST', 'users.list', post=json.dumps(params)) + def user_types_list(): return request('GET', 'user_types.list') - -def users_list(): - return request('GET', 'users.list') - -def resource_types_list(): - return request('GET', 'resource_types.list') - -def resources_list(): - return request('GET', 'resources.list') - -def resource_info(resource=None): - if resource == None: - resource = utils.ask_question("Resource DN") - return request('GET', 'resource.info', get={ 'id': resource }) - -def resource_add(params=None): - if params == None: - params = get_user_input() - - return request('POST', 'resource.add', post=json.dumps(params)) - -def resource_delete(params=None): - if params == None: - params = { - 'id': utils.ask_question("Resource DN to delete", "resource") - } - - return request('POST', 'resource.delete', post=json.dumps(params)) - diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py index 5ca2837..3e12716 100644 --- a/pykolab/xml/__init__.py +++ b/pykolab/xml/__init__.py @@ -1,14 +1,17 @@ from attendee import Attendee from attendee import InvalidAttendeeParticipantStatusError +from attendee import participant_status_label from contact import Contact from contact_reference import ContactReference +from recurrence_rule import RecurrenceRule from event import Event from event import EventIntegrityError from event import InvalidEventDateError from event import event_from_ical from event import event_from_string +from event import event_from_message from utils import to_dt @@ -17,6 +20,7 @@ __all__ = [ "Contact", "ContactReference", "Event", + "RecurrenceRule", "event_from_ical", "event_from_string", "to_dt", diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py index 974af57..fe381ce 100644 --- a/pykolab/xml/attendee.py +++ b/pykolab/xml/attendee.py @@ -1,9 +1,30 @@ import kolabformat from pykolab.translate import _ +from pykolab.translate import N_ from contact_reference import ContactReference +participant_status_labels = { + "NEEDS-ACTION": N_("Needs Action"), + "ACCEPTED": N_("Accepted"), + "DECLINED": N_("Declined"), + "TENTATIVE": N_("Tentatively Accepted"), + "DELEGATED": N_("Delegated"), + "COMPLETED": N_("Completed"), + "IN-PROCESS": N_("In Process"), + # 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"), + } + +def participant_status_label(status): + return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else _(status) + + class Attendee(kolabformat.Attendee): cutype_map = { "INDIVIDUAL": kolabformat.CutypeIndividual, @@ -35,6 +56,13 @@ class Attendee(kolabformat.Attendee): "FALSE": False, } + properties_map = { + 'role': 'get_role', + 'rsvp': 'rsvp', + 'partstat': 'get_participant_status', + 'cutype': 'get_cutype', + } + def __init__( self, email, @@ -76,6 +104,12 @@ class Attendee(kolabformat.Attendee): if not participant_status == None: self.set_participant_status(participant_status) + def copy_from(self, obj): + if isinstance(obj, kolabformat.Attendee): + kolabformat.Attendee.__init__(self, obj) + self.contactreference = ContactReference(obj.contact()) + self.email = self.contactreference.get_email() + def delegate_from(self, delegators): crefs = [] @@ -117,8 +151,11 @@ class Attendee(kolabformat.Attendee): self.setDelegatedTo(list(set(crefs))) - def get_cutype(self): - return self.cutype() + def get_cutype(self, translated=False): + cutype = self.cutype() + if translated: + return self._translate_value(cutype, self.cutype_map) + return cutype def get_delegated_from(self): return self.delegatedFrom() @@ -132,15 +169,30 @@ class Attendee(kolabformat.Attendee): def get_name(self): return self.contactreference.get_name() - def get_participant_status(self): - return self.partStat() + def get_displayname(self): + name = self.contactreference.get_name() + email = self.contactreference.get_email() + return "%s <%s>" % (name, email) if not name == "" else email + + def get_participant_status(self, translated=False): + partstat = self.partStat() + if translated: + return self._translate_value(partstat, self.participant_status_map) + return partstat - def get_role(self): - return self.role() + def get_role(self, translated=False): + role = self.role() + if translated: + return self._translate_value(role, self.role_map) + return role def get_rsvp(self): return self.rsvp() + def _translate_value(self, val, map): + name_map = dict([(v, k) for (k, v) in map.iteritems()]) + return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + def set_cutype(self, cutype): if cutype in self.cutype_map.keys(): self.setCutype(self.cutype_map[cutype]) @@ -151,6 +203,7 @@ class Attendee(kolabformat.Attendee): def set_name(self, name): self.contactreference.set_name(name) + self.setContact(self.contactreference) def set_participant_status(self, participant_status): if participant_status in self.participant_status_map.keys(): @@ -171,6 +224,22 @@ class Attendee(kolabformat.Attendee): def set_rsvp(self, rsvp): self.setRSVP(rsvp) + def to_dict(self): + data = self.contactreference.to_dict() + data.pop('type', None) + + for p, getter in self.properties_map.iteritems(): + val = None + args = {} + if hasattr(self, getter): + if getter.startswith('get_'): + args = dict(translated=True) + val = getattr(self, getter)(**args) + if val is not None: + data[p] = val + + return data + def __str__(self): return self.email diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py index 2871201..c8410f5 100644 --- a/pykolab/xml/contact.py +++ b/pykolab/xml/contact.py @@ -39,5 +39,9 @@ class Contact(kolabformat.Contact): def set_name(self, name): self.setName(name) + def to_ditc(self): + # TODO: implement this + return dict(name=self.name()) + def __str__(self): return kolabformat.writeContact(self) diff --git a/pykolab/xml/contact_reference.py b/pykolab/xml/contact_reference.py index 87d7957..022e2e6 100644 --- a/pykolab/xml/contact_reference.py +++ b/pykolab/xml/contact_reference.py @@ -11,9 +11,18 @@ import kolabformat """ class ContactReference(kolabformat.ContactReference): + properties_map = { + 'email': 'email', + 'name': 'name', + 'type': 'type', + 'uid': 'uid', + } + def __init__(self, email=None): if email is None: kolabformat.ContactReference.__init__(self) + elif isinstance(email, kolabformat.ContactReference): + kolabformat.ContactReference.__init__(self, email.email(), email.name(), email.uid()) else: kolabformat.ContactReference.__init__(self, email) @@ -31,3 +40,15 @@ class ContactReference(kolabformat.ContactReference): def set_name(self, name): self.setName(name) + + def to_dict(self): + data = dict() + + for p, getter in self.properties_map.iteritems(): + val = None + if hasattr(self, getter): + val = getattr(self, getter)() + if val is not None: + data[p] = val + + return data diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index 14128e2..83035d6 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -6,15 +6,20 @@ import kolabformat import pytz import time import uuid +import base64 +import re import pykolab from pykolab import constants from pykolab import utils from pykolab.xml import utils as xmlutils +from pykolab.xml import participant_status_label from pykolab.translate import _ +from os import path from attendee import Attendee from contact_reference import ContactReference +from recurrence_rule import RecurrenceRule log = pykolab.getLogger('pykolab.xml_event') @@ -24,6 +29,21 @@ def event_from_ical(string): def event_from_string(string): return Event(from_string=string) +def event_from_message(message): + event = None + if message.is_multipart(): + for part in message.walk(): + if part.get_content_type() == "application/calendar+xml": + payload = part.get_payload(decode=True) + event = event_from_string(payload) + + # append attachment parts to Event object + elif event and part.has_key('Content-ID'): + event._attachment_parts.append(part) + + return event + + class Event(object): status_map = { "TENTATIVE": kolabformat.StatusTentative, @@ -31,27 +51,81 @@ class Event(object): "CANCELLED": kolabformat.StatusCancelled, } + classification_map = { + "PUBLIC": kolabformat.ClassPublic, + "PRIVATE": kolabformat.ClassPrivate, + "CONFIDENTIAL": kolabformat.ClassConfidential, + } + + alarm_type_map = { + 'EMAIL': kolabformat.Alarm.EMailAlarm, + 'DISPLAY': kolabformat.Alarm.DisplayAlarm, + 'AUDIO': kolabformat.Alarm.AudioAlarm + } + + related_map = { + 'START': kolabformat.Start, + 'END': kolabformat.End + } + + properties_map = { + # property: getter + "uid": "get_uid", + "created": "get_created", + "lastmodified-date": "get_lastmodified", + "sequence": "sequence", + "classification": "get_classification", + "categories": "categories", + "start": "get_start", + "end": "get_end", + "duration": "get_duration", + "transparency": "transparency", + "rrule": "recurrenceRule", + "rdate": "recurrenceDates", + "exdate": "exceptionDates", + "recurrence-id": "recurrenceID", + "summary": "summary", + "description": "description", + "priority": "priority", + "status": "get_status", + "location": "location", + "organizer": "organizer", + "attendee": "get_attendees", + "attach": "attachments", + "url": "url", + "alarm": "alarms", + "x-custom": "customProperties", + # TODO: add to_dict() support for these + # "exception": "exceptions", + } + def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] + self._attachment_parts = [] if from_ical == "": if from_string == "": self.event = kolabformat.Event() else: self.event = kolabformat.readEvent(from_string, False) + self._load_attendees() else: self.from_ical(from_ical) self.uid = self.get_uid() + def _load_attendees(self): + for a in self.event.attendees(): + self._attendees.append(Attendee(a.contact().email(), a.contact().name(), a.rsvp(), a.role(), a.partStat(), a.cutype())) + def add_attendee(self, email, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL", params=None): attendee = Attendee(email, name, rsvp, role, participant_status, cutype, params) self._attendees.append(attendee) self.event.setAttendees(self._attendees) def add_category(self, category): - self._categories.append(category) + self._categories.append(str(category)) self.event.setCategories(self._categories) def add_exception_date(self, _datetime): @@ -97,29 +171,35 @@ class Event(object): # NOTE: Make sure to set() or duplicates may arise for attr in set(event.singletons): - if hasattr(self, 'get_ical_%s' % (attr.lower())): - retval = getattr(self, "get_ical_%s" % attr.lower())() + ical_getter = 'get_ical_%s' % (attr.lower()) + default_getter = 'get_%s' % (attr.lower()) + retval = None + if hasattr(self, ical_getter): + retval = getattr(self, ical_getter)() if retval: event.add(attr.lower(), retval) - - elif hasattr(self, 'get_%s' % (attr.lower())): - retval = getattr(self, "get_%s" % attr.lower())() + elif hasattr(self, default_getter): + retval = getattr(self, default_getter)() if retval: event.add(attr.lower(), retval, encode=0) # NOTE: Make sure to set() or duplicates may arise for attr in set(event.multiple): - if hasattr(self, 'get_ical_%s' % (attr.lower())): - retval = getattr(self, "get_ical_%s" % attr.lower())() - if isinstance(retval, list) and retval: - for _retval in retval: - event.add(attr.lower(), _retval, encode=0) - - elif hasattr(self, 'get_%s' % (attr.lower())): - retval = getattr(self, "get_%s" % attr.lower())() - if isinstance(retval, list) and retval: - for _retval in retval: - event.add(attr.lower(), _retval, encode=0) + ical_getter = 'get_ical_%s' % (attr.lower()) + default_getter = 'get_%s' % (attr.lower()) + retval = None + if hasattr(self, ical_getter): + retval = getattr(self, ical_getter)() + elif hasattr(self, default_getter): + retval = getattr(self, default_getter)() + + if isinstance(retval, list) and retval: + for _retval in retval: + event.add(attr.lower(), _retval, encode=0) + + # copy custom properties to iCal + for cs in self.event.customProperties(): + event.add(cs.identifier, cs.value) cal.add_component(event) @@ -161,12 +241,18 @@ class Event(object): self.event.setAttendees(self._attendees) def from_ical(self, ical): - self.event = kolabformat.Event() if hasattr(icalendar.Event, 'from_ical'): ical_event = icalendar.Event.from_ical(ical) elif hasattr(icalendar.Event, 'from_string'): ical_event = icalendar.Event.from_string(ical) + # use the libkolab calendaring bindings to load the full iCal data + if ical_event.has_key('RRULE') or ical_event.has_key('ATTACH') \ + or [part for part in ical_event.walk() if part.name == 'VALARM']: + self._xml_from_ical(ical) + else: + self.event = kolabformat.Event() + # TODO: Clause the timestamps for zulu suffix causing datetime.datetime # to fail substitution. for attr in set(ical_event.required): @@ -183,13 +269,10 @@ class Event(object): if ical_event.has_key(attr): self.set_from_ical(attr.lower(), ical_event[attr]) - # HACK: use calendaring::EventCal::fromICal() to parse RRULEs - if ical_event.has_key('RRULE'): - from kolab.calendaring import EventCal - event_xml = EventCal() - event_xml.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR") - self.event.setRecurrenceRule(event_xml.recurrenceRule()) - self.event.setExceptionDates(event_xml.exceptionDates()) + def _xml_from_ical(self, ical): + from kolab.calendaring import EventCal + self.event = EventCal() + self.event.fromICal("BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR") def get_attendee_participant_status(self, attendee): return attendee.get_participant_status() @@ -229,20 +312,26 @@ class Event(object): return self._attendees def get_categories(self): - return self.event.categories() + return [str(c) for c in self.event.categories()] def get_classification(self): - return self.classification() + return self.event.classification() def get_created(self): try: - return xmlutils.from_cdatetime(self.event.created(), False) + return xmlutils.from_cdatetime(self.event.created(), True) except ValueError: return datetime.datetime.now() def get_description(self): return self.event.description() + def get_comment(self): + if hasattr(self.event, 'comment'): + return self.event.comment() + else: + return None + def get_duration(self): duration = self.event.duration() if duration and duration.isValid(): @@ -265,9 +354,50 @@ class Event(object): dt = self.get_start() + duration return dt + def get_date_text(self, date_format='%Y-%m-%d', time_format='%H:%M %Z'): + start = self.get_start() + end = self.get_end() + all_day = not hasattr(start, 'date') + start_date = start.date() if not all_day else start + end_date = end.date() if not all_day else end + + if start_date == end_date: + end_format = time_format + else: + end_format = date_format + " " + time_format + + if all_day: + time_format = '' + if start_date == end_date: + return start.strftime(date_format) + + return "%s - %s" % (start.strftime(date_format + " " + time_format), end.strftime(end_format)) + def get_exception_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates()) + def get_attachments(self): + return self.event.attachments() + + def get_attachment_data(self, i): + vattach = self.event.attachments() + if i < len(vattach): + attachment = vattach[i] + uri = attachment.uri() + if uri and uri[0:4] == 'cid:': + # get data from MIME part with matching content-id + cid = '<' + uri[4:] + '>' + for p in self._attachment_parts: + if p['Content-ID'] == cid: + return p.get_payload(decode=True) + else: + return attachment.data() + + return None + + def get_alarms(self): + return self.event.alarms() + def get_ical_attendee(self): # TODO: Formatting, aye? See also the example snippet: # @@ -353,7 +483,11 @@ class Event(object): return self.get_created() def get_ical_dtend(self): - return self.get_end() + dtend = self.get_end() + # shift end by one day on all-day events + if not hasattr(dtend, 'hour'): + dtend = dtend + datetime.timedelta(days=1) + return dtend def get_ical_dtstamp(self): try: @@ -382,12 +516,20 @@ class Event(object): if status in self.status_map.keys(): return status - if status in self.status_map.values(): - return [k for k, v in self.status_map.iteritems() if v == status][0] + return self._translate_value(status, self.status_map) if status else None def get_ical_sequence(self): return str(self.event.sequence()) if self.event.sequence() else None + def get_ical_comment(self): + comment = self.get_comment() + if comment is not None: + return [ comment ] + return None + + def get_location(self): + return self.event.location() + def get_lastmodified(self): try: _datetime = self.event.lastModified() @@ -396,7 +538,7 @@ class Event(object): except: self.__str__() - return xmlutils.from_cdatetime(self.event.lastModified(), False) + return xmlutils.from_cdatetime(self.event.lastModified(), True) def get_organizer(self): organizer = self.event.organizer() @@ -408,11 +550,12 @@ class Event(object): def get_start(self): return xmlutils.from_cdatetime(self.event.start(), True) - def get_status(self): + def get_status(self, translated=False): status = self.event.status() - for key in self.status_map.keys(): - if self.status_map[key] == status: - return key + if translated: + return self._translate_value(status, self.status_map) if status else None + + return status def get_summary(self): return self.event.summary() @@ -428,7 +571,13 @@ class Event(object): def get_sequence(self): return self.event.sequence() - def set_attendee_participant_status(self, attendee, status): + def get_url(self): + return self.event.url() + + def get_transparency(self): + return self.event.transparency() + + def set_attendee_participant_status(self, attendee, status, rsvp=None): """ Set the participant status of an attendee to status. @@ -437,12 +586,20 @@ class Event(object): attendees for this event. """ attendee = self.get_attendee(attendee) - attendee.set_participant_status(status) + + if rsvp is not None: + attendee.set_rsvp(rsvp) + self.event.setAttendees(self._attendees) def set_classification(self, classification): - self.event.setClassification(classification) + if classification in self.classification_map.keys(): + self.event.setClassification(self.classification_map[classification]) + elif classification in self.classification_map.values(): + self.event.setClassification(status) + else: + raise ValueError, _("Invalid classification %r") % (classification) def set_created(self, _datetime=None): if _datetime is None: @@ -451,7 +608,11 @@ class Event(object): self.event.setCreated(xmlutils.to_cdatetime(_datetime, False)) def set_description(self, description): - self.event.setDescription(description) + self.event.setDescription(str(description)) + + def set_comment(self, comment): + if hasattr(self.event, 'setComment'): + self.event.setComment(str(comment)) def set_dtstamp(self, _datetime): self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False)) @@ -477,27 +638,36 @@ class Event(object): for _datetime in _datetimes: self.add_exception_date(_datetime) + def add_custom_property(self, name, value): + if not name.upper().startswith('X-'): + raise ValueError, _("Invalid custom property name %r") % (name) + + props = self.event.customProperties() + props.append(kolabformat.CustomProperty(name.upper(), value)) + self.event.setCustomProperties(props) + def set_from_ical(self, attr, value): + ical_setter = 'set_ical_' + attr + default_setter = 'set_' + attr + if attr == "dtend": self.set_ical_dtend(value.dt) elif attr == "dtstart": self.set_ical_dtstart(value.dt) - elif attr == "duration": - self.set_ical_duration(value) - elif attr == "status": - self.set_ical_status(value) - elif attr == "summary": - self.set_ical_summary(value) - elif attr == "priority": - self.set_ical_priority(value) - elif attr == "sequence": - self.set_ical_sequence(value) - elif attr == "attendee": - self.set_ical_attendee(value) - elif attr == "organizer": - self.set_ical_organizer(value) - elif attr == "uid": - self.set_ical_uid(value) + elif attr == "dtstamp": + self.set_ical_dtstamp(value.dt) + elif attr == "created": + self.set_created(value.dt) + elif attr == "lastmodified": + self.set_lastmodified(value.dt) + elif attr == "categories": + self.add_category(value) + elif attr == "class": + self.set_classification(value) + elif hasattr(self, ical_setter): + getattr(self, ical_setter)(value) + elif hasattr(self, default_setter): + getattr(self, default_setter)(value) def set_ical_attendee(self, _attendee): if isinstance(_attendee, basestring): @@ -540,6 +710,9 @@ class Event(object): att = self.add_attendee(address, name=name, rsvp=rsvp, role=role, participant_status=partstat, cutype=cutype, params=params) def set_ical_dtend(self, dtend): + # shift end by one day on all-day events + if not hasattr(dtend, 'hour'): + dtend = dtend - datetime.timedelta(days=1) self.set_end(dtend) def set_ical_dtstamp(self, dtstamp): @@ -548,6 +721,9 @@ class Event(object): def set_ical_dtstart(self, dtstart): self.set_start(dtstart) + def set_ical_lastmodified(self, lastmod): + self.set_lastmodified(lastmod) + def set_ical_duration(self, value): if value.dt: duration = kolabformat.Duration(value.dt.days, 0, 0, value.dt.seconds, False) @@ -574,14 +750,6 @@ class Event(object): def set_ical_sequence(self, sequence): self.set_sequence(sequence) - def set_ical_status(self, status): - if status in self.status_map.keys(): - self.event.setStatus(self.status_map[status]) - elif status in self.status_map.values(): - self.event.setStatus(status) - else: - raise ValueError, _("Invalid status %r") % (status) - def set_ical_summary(self, summary): self.set_summary(str(summary)) @@ -606,7 +774,7 @@ class Event(object): self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False)) def set_location(self, location): - self.event.setLocation(location) + self.event.setLocation(str(location)) def set_organizer(self, email, name=None): contactreference = ContactReference(email) @@ -621,6 +789,9 @@ class Event(object): def set_sequence(self, sequence): self.event.setSequence(int(sequence)) + def set_url(self, url): + self.event.setUrl(str(url)) + def set_recurrence(self, recurrence): self.event.setRecurrenceRule(recurrence) @@ -657,8 +828,12 @@ class Event(object): self.event.setSummary(summary) def set_uid(self, uid): + self.uid = uid self.event.setUid(str(uid)) + def set_transparency(self, transp): + return self.event.setTransparency(transp) + def __str__(self): event_xml = kolabformat.writeEvent(self.event) @@ -669,6 +844,72 @@ class Event(object): else: raise EventIntegrityError, kolabformat.errorMessage() + def to_dict(self): + data = dict() + + for p, getter in self.properties_map.iteritems(): + val = None + if hasattr(self, getter): + val = getattr(self, getter)() + elif hasattr(self.event, getter): + val = getattr(self.event, getter)() + + if isinstance(val, kolabformat.cDateTime): + val = xmlutils.from_cdatetime(val, True) + elif isinstance(val, kolabformat.vectordatetime): + val = [xmlutils.from_cdatetime(x, True) for x in val] + elif isinstance(val, kolabformat.vectors): + val = [str(x) for x in val] + elif isinstance(val, kolabformat.vectorcs): + for x in val: + data[x.identifier] = x.value + val = None + elif isinstance(val, kolabformat.ContactReference): + val = ContactReference(val).to_dict() + elif isinstance(val, kolabformat.RecurrenceRule): + val = RecurrenceRule(val).to_dict() + elif isinstance(val, kolabformat.vectorattachment): + val = [dict(fmttype=x.mimetype(), label=x.label(), uri=x.uri()) for x in val] + elif isinstance(val, kolabformat.vectoralarm): + val = [self._alarm_to_dict(x) for x in val] + elif isinstance(val, list): + val = [x.to_dict() for x in val if hasattr(x, 'to_dict')] + + if val is not None: + data[p] = val + + return data + + def _alarm_to_dict(self, alarm): + ret = dict( + action=self._translate_value(alarm.type(), self.alarm_type_map), + summary=alarm.summary(), + description=alarm.description(), + trigger=None + ) + + start = alarm.start() + if start and start.isValid(): + ret['trigger'] = xmlutils.from_cdatetime(start, True) + else: + ret['trigger'] = dict(related=self._translate_value(alarm.relativeTo(), self.related_map)) + duration = alarm.relativeStart() + if duration and duration.isValid(): + prefix = '-' if duration.isNegative() else '+' + value = prefix + "P%dW%dDT%dH%dM%dS" % ( + duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds() + ) + ret['trigger']['value'] = re.sub(r"T$", '', re.sub(r"0[WDHMS]", '', value)) + + if alarm.type() == kolabformat.Alarm.EMailAlarm: + ret['attendee'] = [ContactReference(a).to_dict() for a in alarm.attendees()] + + return ret + + def _translate_value(self, val, map): + name_map = dict([(v, k) for (k, v) in map.iteritems()]) + return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + def to_message(self): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase @@ -706,6 +947,39 @@ class Event(object): msg["Subject"] = self.get_uid() + # extract attachment data into separate MIME parts + vattach = self.event.attachments() + i = 0 + for attach in vattach: + if attach.uri(): + continue + + mimetype = attach.mimetype() + (primary, seconday) = mimetype.split('/') + name = attach.label() + if not name: + name = 'unknown.x' + + (basename, suffix) = path.splitext(name) + t = datetime.datetime.now() + cid = "%s.%s.%s%s" % (basename, time.mktime(t.timetuple()), t.microsecond + len(self._attachment_parts), suffix) + + p = MIMEBase(primary, seconday) + p.add_header('Content-Disposition', 'attachment', filename=name) + p.add_header('Content-Transfer-Encoding', 'base64') + p.add_header('Content-ID', '<' + cid + '>') + p.set_payload(base64.b64encode(attach.data())) + + self._attachment_parts.append(p) + + # modify attachment object + attach.setData('', mimetype) + attach.setUri('cid:' + cid, mimetype) + vattach[i] = attach + i += 1 + + self.event.setAttachments(vattach) + part.set_payload(str(self)) part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"') @@ -713,6 +987,10 @@ class Event(object): msg.attach(part) + # append attachment parts + for p in self._attachment_parts: + msg.attach(p) + return msg def to_message_itip(self, from_address, method="REQUEST", participant_status="ACCEPTED", subject=None, message_text=None): @@ -725,6 +1003,7 @@ class Event(object): msg = MIMEMultipart() msg_from = None + attendees = None if method == "REPLY": # TODO: Make user friendly name <email> @@ -737,6 +1016,7 @@ class Event(object): if attendee.get_email() == from_address: # Only the attendee is supposed to be listed in a reply attendee.set_participant_status(participant_status) + attendee.set_rsvp(False) self._attendees = [attendee] self.event.setAttendees(self._attendees) @@ -779,7 +1059,7 @@ class Event(object): msg['Date'] = formatdate(localtime=True) if subject is None: - subject = _("Reservation Request for %s was %s") % (self.get_summary(), _(participant_status)) + subject = _("Invitation for %s was %s") % (self.get_summary(), participant_status_label(participant_status)) msg["Subject"] = subject @@ -798,6 +1078,12 @@ class Event(object): msg.attach(part) + # restore the original list of attendees + # attendees being reduced to the replying attendee above + if attendees is not None: + self._attendees = attendees + self.event.setAttendees(self._attendees) + return msg def is_recurring(self): diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py new file mode 100644 index 0000000..4a0b6c5 --- /dev/null +++ b/pykolab/xml/recurrence_rule.py @@ -0,0 +1,118 @@ +import kolabformat +from pykolab.xml import utils as xmlutils + +""" + def setFrequency(self, *args): return _kolabformat.RecurrenceRule_setFrequency(self, *args) + def frequency(self): return _kolabformat.RecurrenceRule_frequency(self) + def setWeekStart(self, *args): return _kolabformat.RecurrenceRule_setWeekStart(self, *args) + def weekStart(self): return _kolabformat.RecurrenceRule_weekStart(self) + def setEnd(self, *args): return _kolabformat.RecurrenceRule_setEnd(self, *args) + def end(self): return _kolabformat.RecurrenceRule_end(self) + def setCount(self, *args): return _kolabformat.RecurrenceRule_setCount(self, *args) + def count(self): return _kolabformat.RecurrenceRule_count(self) + def setInterval(self, *args): return _kolabformat.RecurrenceRule_setInterval(self, *args) + def interval(self): return _kolabformat.RecurrenceRule_interval(self) + def setBysecond(self, *args): return _kolabformat.RecurrenceRule_setBysecond(self, *args) + def bysecond(self): return _kolabformat.RecurrenceRule_bysecond(self) + def setByminute(self, *args): return _kolabformat.RecurrenceRule_setByminute(self, *args) + def byminute(self): return _kolabformat.RecurrenceRule_byminute(self) + def setByhour(self, *args): return _kolabformat.RecurrenceRule_setByhour(self, *args) + def byhour(self): return _kolabformat.RecurrenceRule_byhour(self) + def setByday(self, *args): return _kolabformat.RecurrenceRule_setByday(self, *args) + def byday(self): return _kolabformat.RecurrenceRule_byday(self) + def setBymonthday(self, *args): return _kolabformat.RecurrenceRule_setBymonthday(self, *args) + def bymonthday(self): return _kolabformat.RecurrenceRule_bymonthday(self) + def setByyearday(self, *args): return _kolabformat.RecurrenceRule_setByyearday(self, *args) + def byyearday(self): return _kolabformat.RecurrenceRule_byyearday(self) + def setByweekno(self, *args): return _kolabformat.RecurrenceRule_setByweekno(self, *args) + def byweekno(self): return _kolabformat.RecurrenceRule_byweekno(self) + def setBymonth(self, *args): return _kolabformat.RecurrenceRule_setBymonth(self, *args) + def bymonth(self): return _kolabformat.RecurrenceRule_bymonth(self) + def isValid(self): return _kolabformat.RecurrenceRule_isValid(self) +""" + +class RecurrenceRule(kolabformat.RecurrenceRule): + frequency_map = { + None: kolabformat.RecurrenceRule.FreqNone, + "YEARLY": kolabformat.RecurrenceRule.Yearly, + "MONTHLY": kolabformat.RecurrenceRule.Monthly, + "WEEKLY": kolabformat.RecurrenceRule.Weekly, + "DAILY": kolabformat.RecurrenceRule.Daily, + "HOURLY": kolabformat.RecurrenceRule.Hourly, + "MINUTELY": kolabformat.RecurrenceRule.Minutely, + "SECONDLY": kolabformat.RecurrenceRule.Secondly + } + + weekday_map = { + "MO": kolabformat.Monday, + "TU": kolabformat.Tuesday, + "WE": kolabformat.Wednesday, + "TH": kolabformat.Thursday, + "FR": kolabformat.Friday, + "SA": kolabformat.Saturday, + "SU": kolabformat.Sunday + } + + properties_map = { + 'frequency': 'get_frequency', + 'interval': 'interval', + 'count': 'count', + 'until': 'end', + 'bymonth': 'bymonth', + 'byday': 'byday', + 'bymonthday':'bymonthday', + 'byyearday': 'byyearday', + 'byweekno': 'byweekno', + 'byhour': 'byhour', + 'byminute': 'byminute', + 'wkst': 'get_weekstart' + } + + def __init__(self, rrule=None): + if rrule == None: + kolabformat.RecurrenceRule.__init__(self) + else: + kolabformat.RecurrenceRule.__init__(self, rrule) + + def get_frequency(self, translated=False): + freq = self.frequency() + if translated: + return self._translate_value(freq, self.frequency_map) + return freq + + def get_weekstart(self, translated=False): + wkst = self.weekStart() + if translated: + return self._translate_value(wkst, self.weekday_map) + return wkst + + def _translate_value(self, val, map): + name_map = dict([(v, k) for (k, v) in map.iteritems()]) + return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + + def to_dict(self): + if not self.isValid() or self.frequency() == kolabformat.RecurrenceRule.FreqNone: + return None + + data = dict() + + for p, getter in self.properties_map.iteritems(): + val = None + args = {} + if hasattr(self, getter): + if getter.startswith('get_'): + args = dict(translated=True) + if hasattr(self, getter): + val = getattr(self, getter)(**args) + if isinstance(val, kolabformat.cDateTime): + val = xmlutils.from_cdatetime(val, True) + elif isinstance(val, kolabformat.vectori): + val = ",".join([int(v) for x in val]) + elif isinstance(val, kolabformat.vectordaypos): + val = ",".join(["%s%s" % (str(x.occurence()) if x.occurence() != 0 else '', self._translate_value(x.weekday(), self.weekday_map)) for x in val]) + if val is not None: + data[p] = val + + return data + + diff --git a/tests/functional/resource_func.py b/tests/functional/resource_func.py index 43aca96..ac80360 100644 --- a/tests/functional/resource_func.py +++ b/tests/functional/resource_func.py @@ -4,7 +4,7 @@ from pykolab import wap_client conf = pykolab.getConf() -def resource_add(type, cn, members=None, owner=None): +def resource_add(type, cn, members=None, owner=None, **kw): if type == None or type == '': raise Exception @@ -18,6 +18,8 @@ def resource_add(type, cn, members=None, owner=None): 'owner': owner } + resource_details.update(kw) + result = wap_client.authenticate(conf.get('ldap', 'bind_dn'), conf.get('ldap', 'bind_pw'), conf.get('kolab', 'primary_domain')) type_id = 0 diff --git a/tests/functional/test_wallace/test_005_resource_add.py b/tests/functional/test_wallace/test_005_resource_add.py index 2de60fb..fc7f3ed 100644 --- a/tests/functional/test_wallace/test_005_resource_add.py +++ b/tests/functional/test_wallace/test_005_resource_add.py @@ -29,8 +29,8 @@ class TestResourceAdd(unittest.TestCase): funcs.purge_resources() self.audi = funcs.resource_add("car", "Audi A4") self.passat = funcs.resource_add("car", "VW Passat") - self.boxter = funcs.resource_add("car", "Porsche Boxter S") - self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ]) + self.boxter = funcs.resource_add("car", "Porsche Boxter S", kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY') + self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ], kolabinvitationpolicy='ACT_ACCEPT') from tests.functional.synchronize import synchronize_once synchronize_once() @@ -56,3 +56,16 @@ class TestResourceAdd(unittest.TestCase): attrs = auth.get_entry_attributes(None, self.cars['dn'], ['*']) self.assertIn('groupofuniquenames', attrs['objectclass']) self.assertEqual(len(attrs['uniquemember']), 3) + self.assertEqual(attrs['kolabinvitationpolicy'], 'ACT_ACCEPT') + + def test_003_get_resource_records(self): + resource_dns = module_resources.resource_record_from_email_address(self.cars['mail']) + self.assertEqual(resource_dns[0], self.cars['dn']) + + resources = module_resources.get_resource_records(resource_dns) + self.assertEqual(len(resources), 4) + + # check for (inherited) kolabinvitationpolicy values (bitmasks) + self.assertEqual(resources[self.cars['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT]) + self.assertEqual(resources[self.audi['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT]) + self.assertEqual(resources[self.boxter['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT_AND_NOTIFY]) diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py index 946cb5f..61b9402 100644 --- a/tests/functional/test_wallace/test_005_resource_invitation.py +++ b/tests/functional/test_wallace/test_005_resource_invitation.py @@ -8,6 +8,10 @@ import uuid from pykolab.imap import IMAP from wallace import module_resources +from pykolab.translate import _ +from pykolab.xml import event_from_message +from pykolab.xml import participant_status_label +from pykolab.itip import events_from_message from email import message_from_string from twisted.trial import unittest @@ -23,13 +27,14 @@ CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT UID:%s -DTSTAMP:20140213T1254140 +DTSTAMP:20140213T125414Z DTSTART;TZID=Europe/London:%s DTEND;TZID=Europe/London:%s SUMMARY:test DESCRIPTION:test ORGANIZER;CN="Doe, John":mailto:john.doe@example.org ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:%s +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Somebody Else:mailto:somebody@else.com TRANSP:OPAQUE END:VEVENT END:VCALENDAR @@ -43,7 +48,7 @@ CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT UID:%s -DTSTAMP:20140215T1254140 +DTSTAMP:20140215T125414Z DTSTART;TZID=Europe/London:%s DTEND;TZID=Europe/London:%s SEQUENCE:2 @@ -90,7 +95,7 @@ CALSCALE:GREGORIAN METHOD:CANCEL BEGIN:VEVENT UID:%s -DTSTAMP:20140218T1254140 +DTSTAMP:20140218T125414Z DTSTART;TZID=Europe/London:20120713T100000 DTEND;TZID=Europe/London:20120713T110000 SUMMARY:test @@ -112,7 +117,7 @@ CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT UID:%s -DTSTAMP:20140213T1254140 +DTSTAMP:20140213T125414Z DTSTART;VALUE=DATE:%s DTEND;VALUE=DATE:%s SUMMARY:test @@ -133,7 +138,7 @@ CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT UID:%s -DTSTAMP:20140213T1254140 +DTSTAMP:20140213T125414Z DTSTART;TZID=Europe/Zurich:%s DTEND;TZID=Europe/Zurich:%s RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10 @@ -184,6 +189,8 @@ class TestResourceInvitation(unittest.TestCase): @classmethod def setup_class(self, *args, **kw): + self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s") + from tests.functional.purge_users import purge_users purge_users() @@ -213,9 +220,12 @@ class TestResourceInvitation(unittest.TestCase): self.boxter = funcs.resource_add("car", "Porsche Boxter S") self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ]) - self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn']) + self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY') self.room2 = funcs.resource_add("confroom", "Conference Room B-222") - self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn']) + self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY') + + self.room3 = funcs.resource_add("confroom", "CEOs Office 303") + self.viprooms = funcs.resource_add("collection", "VIP Rooms", [ self.room3['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL') time.sleep(1) from tests.functional.synchronize import synchronize_once @@ -227,12 +237,14 @@ class TestResourceInvitation(unittest.TestCase): smtp = smtplib.SMTP('localhost', 10026) smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload)) + smtp.quit() - def send_itip_invitation(self, resource_email, start=None, allday=False, template=None): + def send_itip_invitation(self, resource_email, start=None, allday=False, template=None, uid=None): if start is None: start = datetime.datetime.now() - uid = str(uuid.uuid4()) + if uid is None: + uid = str(uuid.uuid4()) if allday: default_template = itip_allday @@ -328,17 +340,14 @@ class TestResourceInvitation(unittest.TestCase): if uid and event_message['subject'] != uid: continue - for part in event_message.walk(): - if part.get_content_type() == "application/calendar+xml": - payload = part.get_payload(decode=True) - found = pykolab.xml.event_from_string(payload) - break - + found = event_from_message(event_message) if found: break time.sleep(1) + imap.disconnect() + return found def purge_mailbox(self, mailbox): @@ -357,12 +366,10 @@ class TestResourceInvitation(unittest.TestCase): def find_resource_by_email(self, email): resource = None - if (email.find(self.audi['mail']) >= 0): - resource = self.audi - if (email.find(self.passat['mail']) >= 0): - resource = self.passat - if (email.find(self.boxter['mail']) >= 0): - resource = self.boxter + for r in [self.audi, self.passat, self.boxter, self.room1, self.room2]: + if (email.find(r['mail']) >= 0): + resource = r + break return resource @@ -379,7 +386,7 @@ class TestResourceInvitation(unittest.TestCase): def test_002_invite_resource(self): uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 10,0,0)) - response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']) self.assertIsInstance(response, email.message.Message) event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid) @@ -387,10 +394,11 @@ class TestResourceInvitation(unittest.TestCase): self.assertEqual(event.get_summary(), "test") + # @depends test_002_invite_resource def test_003_invite_resource_conflict(self): uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0)) - response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail']) self.assertIsInstance(response, email.message.Message) self.assertEqual(self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid), None) @@ -402,7 +410,7 @@ class TestResourceInvitation(unittest.TestCase): uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,7,13, 12,0,0)) # one of the collection members accepted the reservation - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) delegatee = self.find_resource_by_email(accept['from']) @@ -412,7 +420,7 @@ class TestResourceInvitation(unittest.TestCase): self.assertIsInstance(self.check_resource_calendar_event(delegatee['kolabtargetfolder'], uid), pykolab.xml.Event) # resource collection responds with a DELEGATED message - response = self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail']) self.assertIsInstance(response, email.message.Message) self.assertIn("ROLE=NON-PARTICIPANT;RSVP=FALSE", str(response)) @@ -422,13 +430,13 @@ class TestResourceInvitation(unittest.TestCase): uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,4,1, 10,0,0)) - response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']) self.assertIsInstance(response, email.message.Message) self.purge_mailbox(self.john['mailbox']) self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,4,1, 12,0,0)) # conflict with myself - response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']) self.assertIsInstance(response, email.message.Message) event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid) @@ -443,13 +451,13 @@ class TestResourceInvitation(unittest.TestCase): uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,4,24, 12,0,0)) # one of the collection members accepted the reservation - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) delegatee = self.find_resource_by_email(accept['from']) # book that resource for the next day self.send_itip_invitation(delegatee['mail'], datetime.datetime(2014,4,25, 14,0,0)) - accept2 = self.check_message_received("Reservation Request for test was ACCEPTED") + accept2 = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) # re-schedule first booking to a conflicting date self.purge_mailbox(self.john['mailbox']) @@ -457,7 +465,7 @@ class TestResourceInvitation(unittest.TestCase): self.send_itip_update(delegatee['mail'], uid, datetime.datetime(2014,4,25, 12,0,0), template=update_template) # expect response from another member of the initially delegated collection - new_accept = self.check_message_received("Reservation Request for test was ACCEPTED") + new_accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(new_accept, email.message.Message) new_delegatee = self.find_resource_by_email(new_accept['from']) @@ -468,7 +476,7 @@ class TestResourceInvitation(unittest.TestCase): self.assertIsInstance(event, pykolab.xml.Event) # old resource responds with a DELEGATED message - response = self.check_message_received("Reservation Request for test was DELEGATED", delegatee['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, delegatee['mail']) self.assertIsInstance(response, email.message.Message) # old reservation was removed from old delegate's calendar @@ -489,7 +497,7 @@ class TestResourceInvitation(unittest.TestCase): # make new reservation to the now free'd slot self.send_itip_invitation(self.boxter['mail'], datetime.datetime(2014,5,1, 9,0,0)) - response = self.check_message_received("Reservation Request for test was ACCEPTED", self.boxter['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.boxter['mail']) self.assertIsInstance(response, email.message.Message) @@ -500,7 +508,7 @@ class TestResourceInvitation(unittest.TestCase): uid = self.send_itip_invitation(self.cars['mail'], dt) # wait for accept notification - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) delegatee = self.find_resource_by_email(accept['from']) @@ -511,12 +519,12 @@ class TestResourceInvitation(unittest.TestCase): self.send_itip_update(delegatee['mail'], uid, dt, template=update_template) # get response from delegatee - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) self.assertIn(delegatee['mail'], accept['from']) # no delegation response on updates - self.assertEqual(self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail']), None) + self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail']), None) def test_008_allday_reservation(self): @@ -524,7 +532,7 @@ class TestResourceInvitation(unittest.TestCase): uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2), True) - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid) @@ -532,7 +540,7 @@ class TestResourceInvitation(unittest.TestCase): self.assertIsInstance(event.get_start(), datetime.date) uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2, 16,0,0)) - response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail']) self.assertIsInstance(response, email.message.Message) @@ -543,19 +551,19 @@ class TestResourceInvitation(unittest.TestCase): uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,20, 12,0,0), template=itip_recurring.replace(";COUNT=10", "")) - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) # check non-recurring against recurring uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,13, 10,0,0)) - response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail']) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail']) self.assertIsInstance(response, email.message.Message) self.purge_mailbox(self.john['mailbox']) # check recurring against recurring uid3 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,22, 8,0,0), template=itip_recurring) - accept = self.check_message_received("Reservation Request for test was ACCEPTED") + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) self.assertIsInstance(accept, email.message.Message) @@ -570,7 +578,7 @@ class TestResourceInvitation(unittest.TestCase): itip_invalid = itip_invitation.replace("DTSTART;", "X-DTSTART;") self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,24, 19,30,0), template=itip_invalid) - self.assertEqual(self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']), None) + self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']), None) def test_011_owner_info(self): @@ -578,7 +586,7 @@ class TestResourceInvitation(unittest.TestCase): self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,6,19, 16,0,0)) - accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail']) + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room1['mail']) self.assertIsInstance(accept, email.message.Message) respose_text = str(accept.get_payload(0)) self.assertIn(self.jane['mail'], respose_text) @@ -590,27 +598,184 @@ class TestResourceInvitation(unittest.TestCase): self.send_itip_invitation(self.room2['mail'], datetime.datetime(2014,6,19, 16,0,0)) - accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail']) + accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room2['mail']) self.assertIsInstance(accept, email.message.Message) respose_text = str(accept.get_payload(0)) self.assertIn(self.jane['mail'], respose_text) self.assertIn(self.jane['displayname'], respose_text) - def TODO_test_012_owner_notification(self): + def test_012_owner_notification(self): self.purge_mailbox(self.john['mailbox']) self.purge_mailbox(self.jane['mailbox']) - self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,5,4, 13,0,0)) + self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,8,4, 13,0,0)) # check notification message sent to resource owner (jane) - notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'], self.jane['mailbox']) + notify = self.check_message_received(_('Booking for %s has been %s') % (self.room1['cn'], participant_status_label('ACCEPTED')), self.room1['mail'], self.jane['mailbox']) self.assertIsInstance(notify, email.message.Message) - self.assertEqual(notify['From'], self.room1['mail']) - self.assertEqual(notify['Cc'], self.jane['mail']) + + notification_text = str(notify.get_payload()) + self.assertIn(self.john['mail'], notification_text) + self.assertIn(participant_status_label('ACCEPTED'), notification_text) + + self.purge_mailbox(self.john['mailbox']) # check notification sent to collection owner (jane) - self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,5,4, 12,30,0)) + self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,8,4, 12,30,0)) + + # one of the collection members accepted the reservation + accepted = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }) + delegatee = self.find_resource_by_email(accepted['from']) + + notify = self.check_message_received(_('Booking for %s has been %s') % (delegatee['cn'], participant_status_label('ACCEPTED')), delegatee['mail'], self.jane['mailbox']) + self.assertIsInstance(notify, email.message.Message) + self.assertIn(self.john['mail'], notification_text) + + + def test_013_owner_confirmation_accept(self): + self.purge_mailbox(self.john['mailbox']) + self.purge_mailbox(self.jane['mailbox']) + + uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,9,12, 14,0,0)) + + # requester (john) gets a TENTATIVE confirmation + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail']) + self.assertIsInstance(response, email.message.Message) + + event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "test") + self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE') + + # check confirmation message sent to resource owner (jane) + notify = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox']) + self.assertIsInstance(notify, email.message.Message) + + itip_event = events_from_message(notify)[0] + + # resource owner confirms reservation request + itip_reply = itip_event['xml'].to_message_itip(self.jane['mail'], + method="REPLY", + participant_status='ACCEPTED', + message_text="Request accepted", + subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED')) + ) + + smtp = smtplib.SMTP('localhost', 10026) + smtp.sendmail(self.jane['mail'], str(itip_event['organizer']), str(itip_reply)) + smtp.quit() + + # requester (john) now gets the ACCEPTED response + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail']) + self.assertIsInstance(response, email.message.Message) + + event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_status(True), 'CONFIRMED') + self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED') + - notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'], self.jane['mailbox']) + def test_014_owner_confirmation_decline(self): + self.purge_mailbox(self.john['mailbox']) + self.purge_mailbox(self.jane['mailbox']) + + uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,9,14, 9,0,0)) + + # requester (john) gets a TENTATIVE confirmation + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail']) + self.assertIsInstance(response, email.message.Message) + + # check confirmation message sent to resource owner (jane) + notify = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox']) self.assertIsInstance(notify, email.message.Message) + + itip_event = events_from_message(notify)[0] + + # resource owner declines reservation request + itip_reply = itip_event['xml'].to_message_itip(self.jane['mail'], + method="REPLY", + participant_status='DECLINED', + message_text="Request declined", + subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED')) + ) + + smtp = smtplib.SMTP('localhost', 10026) + smtp.sendmail(self.jane['mail'], str(itip_event['organizer']), str(itip_reply)) + smtp.quit() + + # requester (john) now gets the DECLINED response + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.room3['mail']) + self.assertIsInstance(response, email.message.Message) + + # tentative reservation was set to cancelled + event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid) + self.assertEqual(event, None) + #self.assertEqual(event.get_status(True), 'CANCELLED') + #self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'DECLINED') + + + def test_015_owner_confirmation_update(self): + self.purge_mailbox(self.john['mailbox']) + + uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,8,19, 9,0,0), uid="http://a-totally.stupid/?uid") + + # requester (john) gets a TENTATIVE confirmation + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail']) + self.assertIsInstance(response, email.message.Message) + + # check first confirmation message sent to resource owner (jane) + notify1 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox']) + self.assertIsInstance(notify1, email.message.Message) + + itip_event1 = events_from_message(notify1)[0] + self.assertEqual(itip_event1['start'].hour, 9) + + self.purge_mailbox(self.jane['mailbox']) + self.purge_mailbox(self.john['mailbox']) + + # send update with new date (and sequence) + self.send_itip_update(self.room3['mail'], uid, datetime.datetime(2014,8,19, 16,0,0)) + + event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE') + + # check second confirmation message sent to resource owner (jane) + notify2 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox']) + self.assertIsInstance(notify2, email.message.Message) + + itip_event2 = events_from_message(notify2)[0] + self.assertEqual(itip_event2['start'].hour, 16) + + # resource owner declines the first reservation request + itip_reply = itip_event1['xml'].to_message_itip(self.jane['mail'], + method="REPLY", + participant_status='DECLINED', + message_text="Request declined", + subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED')) + ) + smtp = smtplib.SMTP('localhost', 10026) + smtp.sendmail(self.jane['mail'], str(itip_event1['organizer']), str(itip_reply)) + smtp.quit() + + time.sleep(5) + + # resource owner accpets the second reservation request + itip_reply = itip_event2['xml'].to_message_itip(self.jane['mail'], + method="REPLY", + participant_status='ACCEPTED', + message_text="Request accepred", + subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED')) + ) + smtp = smtplib.SMTP('localhost', 10026) + smtp.sendmail(self.jane['mail'], str(itip_event2['organizer']), str(itip_reply)) + smtp.quit() + + # requester (john) now gets the ACCEPTED response + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail']) + self.assertIsInstance(response, email.message.Message) + + event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED') diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py new file mode 100644 index 0000000..9bc808f --- /dev/null +++ b/tests/functional/test_wallace/test_007_invitationpolicy.py @@ -0,0 +1,737 @@ +import time +import pykolab +import smtplib +import email +import datetime +import pytz +import uuid +import kolabformat + +from pykolab.imap import IMAP +from wallace import module_resources + +from pykolab.translate import _ +from pykolab.xml import event_from_message +from pykolab.xml import participant_status_label +from email import message_from_string +from twisted.trial import unittest + +import tests.functional.resource_func as funcs + +conf = pykolab.getConf() + +itip_invitation = """ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:%(uid)s +DTSTAMP:20140213T125414Z +DTSTART;TZID=Europe/Berlin:%(start)s +DTEND;TZID=Europe/Berlin:%(end)s +SUMMARY:%(summary)s +DESCRIPTION:test +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=%(partstat)s;RSVP=TRUE:mailto:%(mailto)s +ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=TENTATIVE;RSVP=FALSE:mailto:somebody@else.com +TRANSP:OPAQUE +SEQUENCE:%(sequence)d +END:VEVENT +END:VCALENDAR +""" + +itip_cancellation = """ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN +CALSCALE:GREGORIAN +METHOD:CANCEL +BEGIN:VEVENT +UID:%(uid)s +DTSTAMP:20140218T125414Z +DTSTART;TZID=Europe/Berlin:20120713T100000 +DTEND;TZID=Europe/Berlin:20120713T110000 +SUMMARY:%(summary)s +DESCRIPTION:test +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE:mailto:%(mailto)s +TRANSP:OPAQUE +SEQUENCE:%(sequence)d +END:VEVENT +END:VCALENDAR +""" + +itip_recurring = """ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:%(uid)s +DTSTAMP:20140213T125414Z +DTSTART;TZID=Europe/Zurich:%(start)s +DTEND;TZID=Europe/Zurich:%(end)s +RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10 +SUMMARY:%(summary)s +DESCRIPTION:test +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=%(partstat)s;RSVP=TRUE:mailto:%(mailto)s +TRANSP:OPAQUE +SEQUENCE:%(sequence)d +END:VEVENT +END:VCALENDAR +""" + +itip_reply = """ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//pykolab-0.6.9-1//kolab.org// +CALSCALE:GREGORIAN +METHOD:REPLY +BEGIN:VEVENT +SUMMARY:%(summary)s +UID:%(uid)s +DTSTART;TZID=Europe/Berlin;VALUE=DATE-TIME:%(start)s +DTEND;TZID=Europe/Berlin;VALUE=DATE-TIME:%(end)s +DTSTAMP;VALUE=DATE-TIME:20140706T171038Z +ORGANIZER;CN="Doe, John":MAILTO:%(organizer)s +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s +PRIORITY:0 +SEQUENCE:%(sequence)d +END:VEVENT +END:VCALENDAR +""" + +mime_message = """MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_c8894dbdb8baeedacae836230e3436fd" +From: "Doe, John" <john.doe@example.org> +Date: Tue, 25 Feb 2014 13:54:14 +0100 +Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org> +To: %s +Subject: "test" + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: quoted-printable + +*test* + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: text/calendar; charset=UTF-8; method=%s; name=event.ics +Content-Disposition: attachment; filename=event.ics +Content-Transfer-Encoding: 8bit + +%s +--=_c8894dbdb8baeedacae836230e3436fd-- +""" + +class TestWallaceInvitationpolicy(unittest.TestCase): + + john = None + itip_reply_subject = None + + @classmethod + def setUp(self): + """ Compatibility for twisted.trial.unittest + """ + if not self.john: + self.setup_class() + + @classmethod + def setup_class(self, *args, **kw): + self.itip_reply_subject = _('"%(summary)s" has been %(status)s') + + from tests.functional.purge_users import purge_users + purge_users() + + self.john = { + 'displayname': 'John Doe', + 'mail': 'john.doe@example.org', + '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', + 'kolabinvitationpolicy': ['ACT_UPDATE_AND_NOTIFY','ACT_MANUAL'] + } + + self.jane = { + 'displayname': 'Jane Manager', + 'mail': 'jane.manager@example.org', + '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'] + } + + self.jack = { + 'displayname': 'Jack Tentative', + 'mail': 'jack.tentative@example.org', + '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'] + } + + self.mark = { + 'displayname': 'Mark German', + 'mail': 'mark.german@example.org', + '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', + 'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE_AND_NOTIFY'] + } + + self.external = { + 'displayname': 'Bob External', + 'mail': 'bob.external@gmail.com' + } + + from tests.functional.user_add import user_add + user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage']) + user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage']) + user_add("Jack", "Tentative", kolabinvitationpolicy=self.jack['kolabinvitationpolicy'], preferredlanguage=self.jack['preferredlanguage']) + user_add("Mark", "German", kolabinvitationpolicy=self.mark['kolabinvitationpolicy'], preferredlanguage=self.mark['preferredlanguage']) + + time.sleep(1) + from tests.functional.synchronize import synchronize_once + synchronize_once() + + def send_message(self, itip_payload, to_addr, from_addr=None, method="REQUEST"): + if from_addr is None: + from_addr = self.john['mail'] + + smtp = smtplib.SMTP('localhost', 10026) + smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, method, itip_payload)) + + def send_itip_invitation(self, attendee_email, start=None, allday=False, template=None, summary="test", sequence=0, partstat='NEEDS-ACTION'): + if start is None: + start = datetime.datetime.now() + + uid = str(uuid.uuid4()) + + if allday: + default_template = itip_allday + end = start + datetime.timedelta(days=1) + date_format = '%Y%m%d' + else: + end = start + datetime.timedelta(hours=4) + default_template = itip_invitation + date_format = '%Y%m%dT%H%M%S' + + self.send_message((template if template is not None else default_template) % { + 'uid': uid, + 'start': start.strftime(date_format), + 'end': end.strftime(date_format), + 'mailto': attendee_email, + 'summary': summary, + 'sequence': sequence, + 'partstat': partstat + }, + attendee_email) + + return uid + + def send_itip_update(self, attendee_email, uid, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'): + if start is None: + start = datetime.datetime.now() + + end = start + datetime.timedelta(hours=4) + self.send_message((template if template is not None else itip_invitation) % { + 'uid': uid, + 'start': start.strftime('%Y%m%dT%H%M%S'), + 'end': end.strftime('%Y%m%dT%H%M%S'), + 'mailto': attendee_email, + 'summary': summary, + 'sequence': sequence, + 'partstat': partstat + }, + attendee_email) + + return uid + + def send_itip_reply(self, uid, attendee_email, mailto, start=None, template=None, summary="test", sequence=0, partstat='ACCEPTED'): + if start is None: + start = datetime.datetime.now() + + end = start + datetime.timedelta(hours=4) + self.send_message((template if template is not None else itip_reply) % { + 'uid': uid, + 'start': start.strftime('%Y%m%dT%H%M%S'), + 'end': end.strftime('%Y%m%dT%H%M%S'), + 'mailto': attendee_email, + 'organizer': mailto, + 'summary': summary, + 'sequence': sequence, + 'partstat': partstat + }, + mailto, + attendee_email, + method='REPLY') + + return uid + + def send_itip_cancel(self, attendee_email, uid, summary="test", sequence=1): + self.send_message(itip_cancellation % { + 'uid': uid, + 'mailto': attendee_email, + 'summary': summary, + 'sequence': sequence, + }, + attendee_email, + method='CANCEL') + + return uid + + def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None): + if start is None: + start = datetime.datetime.now(pytz.timezone("Europe/Berlin")) + if user is None: + user = self.john + if attendees is None: + attendees = [self.jane] + + end = start + datetime.timedelta(hours=4) + + event = pykolab.xml.Event() + event.set_start(start) + event.set_end(end) + event.set_organizer(user['mail'], user['displayname']) + + for attendee in attendees: + event.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True) + + event.set_summary(summary) + event.set_sequence(sequence) + + # create event with attachment + vattach = event.get_attachments() + attachment = kolabformat.Attachment() + attachment.setLabel('attach.txt') + attachment.setData('This is a text attachment', 'text/plain') + vattach.append(attachment) + event.event.setAttachments(vattach) + + imap = IMAP() + imap.connect() + + mailbox = imap.folder_quote(user['kolabtargetfolder']) + imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda") + imap.imap.m.select(mailbox) + + result = imap.imap.m.append( + mailbox, + None, + None, + event.to_message().as_string() + ) + + return event.get_uid() + + def update_calendar_event(self, uid, start=None, summary=None, sequence=0, user=None): + if user is None: + user = self.john + + event = self.check_user_calendar_event(user['kolabtargetfolder'], uid) + if event: + if start is not None: + event.set_start(start) + if summary is not None: + event.set_summary(summary) + if sequence is not None: + event.set_sequence(sequence) + + imap = IMAP() + imap.connect() + + mailbox = imap.folder_quote(user['kolabtargetfolder']) + imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda") + imap.imap.m.select(mailbox) + + return imap.imap.m.append( + mailbox, + None, + None, + event.to_message().as_string() + ) + + return False + + def check_message_received(self, subject, from_addr=None, mailbox=None): + if mailbox is None: + mailbox = self.john['mailbox'] + + imap = IMAP() + imap.connect() + + mailbox = imap.folder_quote(mailbox) + imap.set_acl(mailbox, "cyrus-admin", "lrs") + imap.imap.m.select(mailbox) + + found = None + retries = 15 + + while not found and retries > 0: + retries -= 1 + + typ, data = imap.imap.m.search(None, '(UNDELETED HEADER FROM "%s")' % (from_addr) if from_addr else 'UNDELETED') + for num in data[0].split(): + typ, msg = imap.imap.m.fetch(num, '(RFC822)') + message = message_from_string(msg[0][1]) + if message['Subject'] == subject: + found = message + break + + time.sleep(1) + + imap.disconnect() + + return found + + def check_user_calendar_event(self, mailbox, uid=None): + imap = IMAP() + imap.connect() + + mailbox = imap.folder_quote(mailbox) + imap.set_acl(mailbox, "cyrus-admin", "lrs") + imap.imap.m.select(mailbox) + + found = None + retries = 15 + + while not found and retries > 0: + retries -= 1 + + typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")') + for num in data[0].split(): + typ, data = imap.imap.m.fetch(num, '(RFC822)') + event_message = message_from_string(data[0][1]) + + # return matching UID or first event found + if uid and event_message['subject'] != uid: + continue + + found = event_from_message(event_message) + if found: + break + + time.sleep(1) + + return found + + def purge_mailbox(self, mailbox): + imap = IMAP() + imap.connect() + mailbox = imap.folder_quote(mailbox) + imap.set_acl(mailbox, "cyrus-admin", "lrwcdest") + imap.imap.m.select(mailbox) + + typ, data = imap.imap.m.search(None, 'ALL') + for num in data[0].split(): + imap.imap.m.store(num, '+FLAGS', '\\Deleted') + + imap.imap.m.expunge() + imap.disconnect() + + + def test_001_invite_accept_udate(self): + start = datetime.datetime(2014,8,13, 10,0,0) + uid = self.send_itip_invitation(self.jane['mail'], start) + + response = self.check_message_received(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) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "test") + + # send update with the same sequence: no re-scheduling + self.send_itip_update(self.jane['mail'], uid, start, summary="test updated", sequence=0, partstat='ACCEPTED') + + time.sleep(10) + event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "test updated") + self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted) + + + # @depends on test_001_invite_user + def test_002_invite_conflict_reject(self): + uid = self.send_itip_invitation(self.jane['mail'], datetime.datetime(2014,8,13, 11,0,0), summary="test2") + + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jane['mail']) + self.assertIsInstance(response, email.message.Message) + + event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "test2") + + + def test_003_invite_accept_tentative(self): + self.purge_mailbox(self.john['mailbox']) + + uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,24, 8,0,0)) + + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail']) + self.assertIsInstance(response, email.message.Message) + + + def test_004_copy_to_calendar(self): + self.purge_mailbox(self.john['mailbox']) + + self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 8,0,0)) + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail']) + self.assertIsInstance(response, email.message.Message) + + # send conflicting request to jack + uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 10,0,0), summary="test2") + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jack['mail']) + self.assertEqual(response, None, "No reply expected") + + event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "test2") + self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction) + + + def test_005_invite_rescheduling_accept(self): + self.purge_mailbox(self.john['mailbox']) + + start = datetime.datetime(2014,8,14, 9,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.send_itip_invitation(self.jane['mail'], start) + + response = self.check_message_received(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) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "test") + + self.purge_mailbox(self.john['mailbox']) + + # send update with new date and incremented sequence + new_start = datetime.datetime(2014,8,15, 15,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + self.send_itip_update(self.jane['mail'], uid, new_start, summary="test", sequence=1) + + response = self.check_message_received(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) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_start(), new_start) + self.assertEqual(event.get_sequence(), 1) + + + def test_005_invite_rescheduling_reject(self): + self.purge_mailbox(self.john['mailbox']) + self.purge_mailbox(self.jack['kolabtargetfolder']) + + start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.send_itip_invitation(self.jack['mail'], start) + + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail']) + self.assertIsInstance(response, email.message.Message) + + # send update with new but conflicting date and incremented sequence + self.create_calendar_event(datetime.datetime(2014,8,10, 10,30,0, tzinfo=pytz.timezone("Europe/Berlin")), user=self.jack) + new_start = datetime.datetime(2014,8,10, 9,30,0, tzinfo=pytz.timezone("Europe/Berlin")) + self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1) + + response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.jack['mail']) + self.assertEqual(response, None) + + # verify re-scheduled copy in jack's calendar with NEEDS-ACTION + event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_start(), new_start) + self.assertEqual(event.get_sequence(), 1) + + attendee = event.get_attendee(self.jack['mail']) + self.assertTrue(attendee.get_rsvp()) + self.assertEqual(attendee.get_participant_status(), kolabformat.PartNeedsAction) + + + def test_006_invitation_reply(self): + self.purge_mailbox(self.john['mailbox']) + + start = datetime.datetime(2014,8,18, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.create_calendar_event(start, user=self.john) + + event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + + # send a reply from jane to john + self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start) + + # check for the updated event in john's calendar + time.sleep(10) + event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + + attendee = event.get_attendee(self.jane['mail']) + self.assertIsInstance(attendee, pykolab.xml.Attendee) + self.assertEqual(attendee.get_participant_status(), kolabformat.PartAccepted) + + # check attachments in update event + attachments = event.get_attachments() + self.assertEqual(len(attachments), 1) + self.assertEqual(event.get_attachment_data(0), 'This is a text attachment') + + + def test_007_invitation_cancel(self): + self.purge_mailbox(self.john['mailbox']) + + uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled") + + response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':participant_status_label('ACCEPTED') }, self.jane['mail']) + self.assertIsInstance(response, email.message.Message) + + self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled") + + time.sleep(10) + event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_summary(), "cancelled") + self.assertEqual(event.get_status(True), 'CANCELLED') + self.assertTrue(event.get_transparency()) + + + def test_008_inivtation_reply_notify(self): + self.purge_mailbox(self.john['mailbox']) + + start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.mark, self.jack]) + + # send a reply from jane to john + self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start) + + # check for notification message + # this notification should be suppressed until mark has replied, too + notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail']) + self.assertEqual(notification, None) + + # send a reply from mark to john + self.send_itip_reply(uid, self.mark['mail'], self.john['mail'], start=start, partstat='ACCEPTED') + + notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail']) + self.assertIsInstance(notification, email.message.Message) + + notification_text = str(notification.get_payload()); + self.assertIn(self.jane['mail'], notification_text) + self.assertIn(_("PENDING"), notification_text) + + self.purge_mailbox(self.john['mailbox']) + + # send a reply from mark to john + self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='ACCEPTED') + + # this triggers an additional notification + notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail']) + self.assertIsInstance(notification, email.message.Message) + + notification_text = str(notification.get_payload()); + self.assertNotIn(_("PENDING"), notification_text) + + + def test_008_notify_translated(self): + self.purge_mailbox(self.mark['mailbox']) + + start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.create_calendar_event(start, user=self.mark, attendees=[self.jane]) + + # send a reply from jane to mark + self.send_itip_reply(uid, self.jane['mail'], self.mark['mail'], start=start) + + # change translations to de_DE + pykolab.translate.setUserLanguage(self.mark['preferredlanguage']) + notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.mark['mail'], self.mark['mailbox']) + self.assertIsInstance(notification, email.message.Message) + + notification_text = str(notification.get_payload()); + self.assertIn(self.jane['mail'], notification_text) + self.assertIn(participant_status_label("ACCEPTED")+":", notification_text) + + # reset localization + pykolab.translate.setUserLanguage(conf.get('kolab','default_locale')) + + + def test_009_outdated_reply(self): + self.purge_mailbox(self.john['mailbox']) + + start = datetime.datetime(2014,9,2, 11,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.create_calendar_event(start, user=self.john, sequence=2) + + # send a reply from jane to john + self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start, sequence=1) + + # verify jane's attendee status was not updated + time.sleep(10) + event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + self.assertEqual(event.get_sequence(), 2) + self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartNeedsAction) + + + def test_010_partstat_update_propagation(self): + # ATTENTION: this test requires wallace.invitationpolicy_autoupdate_other_attendees_on_reply to be enabled in config + + start = datetime.datetime(2014,8,21, 13,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack, self.external]) + + event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + + # send invitations to jack and jane + event_itip = event.as_string_itip() + self.send_itip_invitation(self.jane['mail'], start, template=event_itip) + self.send_itip_invitation(self.jack['mail'], start, template=event_itip) + + # wait for replies from jack and jane to be processed and propagated + time.sleep(10) + event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + + # check updated event in organizer's calendar + self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted) + self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative) + + # check updated partstats in jane's calendar + janes = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid) + self.assertEqual(janes.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted) + self.assertEqual(janes.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative) + + # check updated partstats in jack's calendar + jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid) + self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted) + self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative) + + # PART 2: create conflicting event in jack's calendar + new_start = datetime.datetime(2014,8,21, 6,0,0, tzinfo=pytz.timezone("Europe/Berlin")) + self.create_calendar_event(new_start, user=self.jack, attendees=[], summary="blocker") + + # re-schedule initial event to new date + self.update_calendar_event(uid, start=new_start, sequence=1, user=self.john) + self.send_itip_update(self.jane['mail'], uid, new_start, summary="test (updated)", sequence=1) + self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1) + + # wait for replies to be processed and propagated + time.sleep(10) + event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + + # check updated event in organizer's calendar (jack didn't reply yet) + self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted) + self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative) + + # check partstats in jack's calendar: jack's status should remain needs-action + jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid) + self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted) + self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction) + + diff --git a/tests/functional/user_add.py b/tests/functional/user_add.py index 4939f93..b1b37f1 100644 --- a/tests/functional/user_add.py +++ b/tests/functional/user_add.py @@ -4,7 +4,7 @@ from pykolab import wap_client conf = pykolab.getConf() -def user_add(givenname, sn, preferredlanguage='en_US'): +def user_add(givenname, sn, preferredlanguage='en_US', **kw): if givenname == None: raise Exception @@ -25,6 +25,8 @@ def user_add(givenname, sn, preferredlanguage='en_US'): 'userpassword': 'Welcome2KolabSystems' } + user_details.update(kw) + login = conf.get('ldap', 'bind_dn') password = conf.get('ldap', 'bind_pw') domain = conf.get('kolab', 'primary_domain') diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py index 9da93c7..d7584e3 100644 --- a/tests/unit/test-002-attendee.py +++ b/tests/unit/test-002-attendee.py @@ -1,7 +1,9 @@ import datetime import unittest +import kolabformat from pykolab.xml import Attendee +from pykolab.xml import participant_status_label class TestEventXML(unittest.TestCase): attendee = Attendee("jane@doe.org") @@ -101,5 +103,30 @@ class TestEventXML(unittest.TestCase): self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 2][0], "INDIVIDUAL") self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 3][0], "RESOURCE") + def test_018_partstat_label(self): + self.assertEqual(participant_status_label('NEEDS-ACTION'), "Needs Action") + self.assertEqual(participant_status_label(kolabformat.PartTentative), "Tentatively Accepted") + self.assertEqual(participant_status_label('UNKNOWN'), "UNKNOWN") + + def test_020_to_dict(self): + name = "Doe, Jane" + role = 'OPT-PARTICIPANT' + cutype = 'RESOURCE' + partstat = 'ACCEPTED' + self.attendee.set_name(name) + self.attendee.set_rsvp(True) + self.attendee.set_role(role) + self.attendee.set_cutype(cutype) + self.attendee.set_participant_status(partstat) + + data = self.attendee.to_dict() + self.assertIsInstance(data, dict) + self.assertEqual(data['role'], role) + self.assertEqual(data['cutype'], cutype) + self.assertEqual(data['partstat'], partstat) + self.assertEqual(data['name'], name) + self.assertEqual(data['email'], 'jane@doe.org') + self.assertTrue(data['rsvp']) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py index 61ea8ec..1f54419 100644 --- a/tests/unit/test-003-event.py +++ b/tests/unit/test-003-event.py @@ -1,3 +1,4 @@ +import re import datetime import pytz import sys @@ -11,6 +12,233 @@ from pykolab.xml import EventIntegrityError from pykolab.xml import InvalidAttendeeParticipantStatusError from pykolab.xml import InvalidEventDateError from pykolab.xml import event_from_ical +from pykolab.xml import event_from_string +from pykolab.xml import event_from_message + +ical_event = """ +BEGIN:VEVENT +UID:7a35527d-f783-4b58-b404-b1389bd2fc57 +DTSTAMP;VALUE=DATE-TIME:20140407T122311Z +CREATED;VALUE=DATE-TIME:20140407T122245Z +LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z +DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000 +DURATION:PT1H30M0S +RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10 +EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000 +EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000 +SUMMARY:Summary +LOCATION:Location +DESCRIPTION:Description\\n2 lines +CATEGORIES:Personal +TRANSP:OPAQUE +PRIORITY:2 +SEQUENCE:2 +CLASS:PUBLIC +ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP + E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager@example.org +ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA + LSE:MAILTO:max@imum.com +ORGANIZER;CN=Doe\, John:mailto:john.doe@example.org +URL:http://somelink.com/foo +ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn + g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i + ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF + xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX + V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS + bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U + eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV + m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg + lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d + ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q + QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3 + ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo + ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf + vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr + tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/ + Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg== +ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh + pcyBpcyBhIHRleHQgZmlsZQo= +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT30M +END:VALARM +END:VEVENT +""" + +xml_event = """ +<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"> + <vcalendar> + <properties> + <prodid> + <text>Libkolabxml-1.1</text> + </prodid> + <version> + <text>2.0</text> + </version> + <x-kolab-version> + <text>3.1.0</text> + </x-kolab-version> + </properties> + <components> + <vevent> + <properties> + <uid> + <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text> + </uid> + <created> + <date-time>2014-07-07T01:28:23Z</date-time> + </created> + <dtstamp> + <date-time>2014-07-07T01:28:23Z</date-time> + </dtstamp> + <sequence> + <integer>1</integer> + </sequence> + <class> + <text>PUBLIC</text> + </class> + <dtstart> + <parameters> + <tzid> + <text>/kolab.org/Europe/London</text> + </tzid> + </parameters> + <date-time>2014-08-13T10:00:00</date-time> + </dtstart> + <dtend> + <parameters> + <tzid><text>/kolab.org/Europe/London</text></tzid> + </parameters> + <date-time>2014-08-13T14:00:00</date-time> + </dtend> + <rrule> + <recur> + <freq>DAILY</freq> + <until> + <date>2014-07-25</date> + </until> + </recur> + </rrule> + <exdate> + <parameters> + <tzid> + <text>/kolab.org/Europe/Berlin</text> + </tzid> + </parameters> + <date>2014-07-19</date> + <date>2014-07-26</date> + <date>2014-07-12</date> + <date>2014-07-13</date> + <date>2014-07-20</date> + <date>2014-07-27</date> + <date>2014-07-05</date> + <date>2014-07-06</date> + </exdate> + <summary> + <text>test</text> + </summary> + <description> + <text>test</text> + </description> + <priority> + <integer>5</integer> + </priority> + <status> + <text>CANCELLED</text> + </status> + <location> + <text>Room 101</text> + </location> + <organizer> + <parameters> + <cn><text>Doe, John</text></cn> + </parameters> + <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address> + </organizer> + <attendee> + <parameters> + <partstat><text>ACCEPTED</text></partstat> + <role><text>REQ-PARTICIPANT</text></role> + <rsvp><boolean>true</boolean></rsvp> + </parameters> + <cal-address>mailto:%3Cjane%40example.org%3E</cal-address> + </attendee> + <attendee> + <parameters> + <partstat><text>TENTATIVE</text></partstat> + <role><text>OPT-PARTICIPANT</text></role> + </parameters> + <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address> + </attendee> + <attach> + <parameters> + <fmttype> + <text>text/html</text> + </fmttype> + <x-label> + <text>noname.1395223627.5555</text> + </x-label> + </parameters> + <uri>cid:noname.1395223627.5555</uri> + </attach> + <x-custom> + <identifier>X-MOZ-RECEIVED-DTSTAMP</identifier> + <value>20140224T155612Z</value> + </x-custom> + <x-custom> + <identifier>X-GWSHOW-AS</identifier> + <value>BUSY</value> + </x-custom> + </properties> + <components> + <valarm> + <properties> + <action> + <text>DISPLAY</text> + </action> + <description> + <text>alarm 1</text> + </description> + <trigger> + <parameters> + <related> + <text>START</text> + </related> + </parameters> + <duration>-PT2H</duration> + </trigger> + </properties> + </valarm> + <valarm> + <properties> + <action> + <text>EMAIL</text> + </action> + <summary> + <text>test</text> + </summary> + <description> + <text>alarm 2</text> + </description> + <attendee> + <cal-address>mailto:%3Cjohn.die%40example.org%3E</cal-address> + </attendee> + <trigger> + <parameters> + <related> + <text>START</text> + </related> + </parameters> + <duration>-P1D</duration> + </trigger> + </properties> + </valarm> + </components> + </vevent> + </components> + </vcalendar> +</icalendar> +""" class TestEventXML(unittest.TestCase): event = Event() @@ -117,25 +345,23 @@ class TestEventXML(unittest.TestCase): def test_018_load_from_ical(self): ical_str = """BEGIN:VCALENDAR VERSION:2.0 -PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN +PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject + 2.1.3//EN CALSCALE:GREGORIAN -BEGIN:VEVENT -DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000 -DURATION:PT1H30M0S -RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10 -EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000 -EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000 -UID:7a35527d-f783-4b58-b404-b1389bd2fc57 -ATTENDEE;CN="Doe, Jane";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED - ;ROLE=REQ-PARTICIPANT;RSVP=FALSE:MAILTO:jane@doe.org -ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION - ;ROLE=OPT-PARTICIPANT;RSVP=FALSE:MAILTO:max@imum.com -SEQUENCE:2 -END:VEVENT -END:VCALENDAR -""" +METHOD:REQUEST + """ + ical_event + "END:VCALENDAR" + ical = icalendar.Calendar.from_ical(ical_str) event = event_from_ical(ical.walk('VEVENT')[0].to_ical()) + + self.assertEqual(event.get_location(), "Location") + self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11+00:00") + self.assertEqual(event.get_description(), "Description\n2 lines") + self.assertEqual(event.get_url(), "http://somelink.com/foo") + self.assertEqual(event.get_transparency(), False) + self.assertEqual(event.get_categories(), ["Personal"]) + self.assertEqual(event.get_priority(), '2') + self.assertEqual(event.get_classification(), kolabformat.ClassPublic) self.assertEqual(event.get_attendee_by_email("max@imum.com").get_cutype(), kolabformat.CutypeResource) self.assertEqual(event.get_sequence(), 2) self.assertTrue(event.is_recurring()) @@ -144,17 +370,62 @@ END:VCALENDAR self.assertEqual(str(event.get_end()), "2014-05-23 12:30:00+01:00") self.assertEqual(len(event.get_exception_dates()), 2) self.assertIsInstance(event.get_exception_dates()[0], datetime.datetime) + self.assertEqual(len(event.get_alarms()), 1) + self.assertEqual(len(event.get_attachments()), 2) + + def test_018_ical_to_message(self): + event = event_from_ical(ical_event) + message = event.to_message() + + self.assertTrue(message.is_multipart()) + self.assertEqual(message['Subject'], event.uid) + self.assertEqual(message['X-Kolab-Type'], 'application/x-vnd.kolab.event') + + parts = [p for p in message.walk()] + attachments = event.get_attachments(); + + self.assertEqual(len(parts), 5) + self.assertEqual(parts[1].get_content_type(), 'text/plain') + self.assertEqual(parts[2].get_content_type(), 'application/calendar+xml') + self.assertEqual(parts[3].get_content_type(), 'image/png') + self.assertEqual(parts[4].get_content_type(), 'text/plain') + self.assertEqual(parts[2]['Content-ID'], None) + self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:]) + self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:]) + + def test_018_ical_allday_events(self): + ical = """BEGIN:VEVENT +UID:ffffffff-f783-4b58-b404-b1389bd2ffff +DTSTAMP;VALUE=DATE-TIME:20140407T122311Z +CREATED;VALUE=DATE-TIME:20140407T122245Z +DTSTART;VALUE=DATE:20140823 +DTEND;VALUE=DATE:20140824 +SUMMARY:All day +DESCRIPTION:One single day +TRANSP:OPAQUE +CLASS:PUBLIC +END:VEVENT +""" + event = event_from_ical(ical) + self.assertEqual(str(event.get_start()), "2014-08-23") + self.assertEqual(str(event.get_end()), "2014-08-23") + self.assertEqual(str(event.get_ical_dtend()), "2014-08-24") + self.assertTrue(re.match('.*<dtend>\s*<date>2014-08-23</date>', str(event), re.DOTALL)) def test_019_as_string_itip(self): self.event.set_summary("test") self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London"))) self.event.set_end(datetime.datetime(2014, 05, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London"))) + self.event.set_sequence(3) + self.event.add_custom_property('X-Custom', 'check') ical = icalendar.Calendar.from_ical(self.event.as_string_itip()) event = ical.walk('VEVENT')[0] self.assertEqual(event['uid'], self.event.get_uid()) self.assertEqual(event['summary'], "test") + self.assertEqual(event['sequence'], 3) + self.assertEqual(event['X-CUSTOM'], "check") self.assertIsInstance(event['dtstamp'].dt, datetime.datetime) def test_020_calendaring_recurrence(self): @@ -214,6 +485,94 @@ END:VCALENDAR self.assertEqual(self.event.get_next_occurence(_start), None) self.assertEqual(self.event.get_last_occurrence(), None) + def test_022_load_from_xml(self): + event = event_from_string(xml_event) + self.assertEqual(event.uid, '75c740bb-b3c6-442c-8021-ecbaeb0a025e') + self.assertEqual(event.get_attendee_by_email("jane@example.org").get_participant_status(), kolabformat.PartAccepted) + self.assertEqual(event.get_sequence(), 1) + self.assertIsInstance(event.get_start(), datetime.datetime) + self.assertEqual(str(event.get_start()), "2014-08-13 10:00:00+00:00") + + def test_023_load_from_message(self): + event = event_from_message(event_from_ical(ical_event).to_message()) + event.set_sequence(3) + + message = event.to_message() + self.assertTrue(message.is_multipart()) + + # check attachment MIME parts are kept + parts = [p for p in message.walk()] + attachments = event.get_attachments(); + + self.assertEqual(len(parts), 5) + self.assertEqual(parts[3].get_content_type(), 'image/png') + self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:]) + self.assertEqual(parts[4].get_content_type(), 'text/plain') + self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:]) + self.assertEqual(event.get_attachment_data(1), 'This is a text file') + + def test_024_bogus_itip_data(self): + # DTSTAMP contains an invalid date/time value + vevent = """BEGIN:VEVENT +UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 +DTSTAMP:20120713T1254140 +DTSTART;TZID=Europe/London:20120713T100000 +DTEND;TZID=Europe/London:20120713T110000 +SUMMARY:test +DESCRIPTION:test +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailt + o:jane.doe@example.org +ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailt + o:user.external@example.com +SEQUENCE:1 +TRANSP:OPAQUE +END:VEVENT +""" + event = event_from_ical(vevent) + self.assertRaises(EventIntegrityError, event.to_message) + + def test_025_to_dict(self): + data = event_from_string(xml_event).to_dict() + + self.assertIsInstance(data, dict) + self.assertIsInstance(data['start'], datetime.datetime) + self.assertIsInstance(data['end'], datetime.datetime) + self.assertIsInstance(data['created'], datetime.datetime) + self.assertIsInstance(data['lastmodified-date'], datetime.datetime) + self.assertEqual(data['uid'], '75c740bb-b3c6-442c-8021-ecbaeb0a025e') + self.assertEqual(data['summary'], 'test') + self.assertEqual(data['location'], 'Room 101') + self.assertEqual(data['description'], 'test') + self.assertEqual(data['priority'], 5) + self.assertEqual(data['status'], 'CANCELLED') + self.assertEqual(data['sequence'], 1) + self.assertEqual(data['transparency'], False) + self.assertEqual(data['X-GWSHOW-AS'], 'BUSY') + + self.assertIsInstance(data['organizer'], dict) + self.assertEqual(data['organizer']['email'], 'john@example.org') + + self.assertEqual(len(data['attendee']), 2) + self.assertIsInstance(data['attendee'][0], dict) + + self.assertEqual(len(data['attach']), 1) + self.assertIsInstance(data['attach'][0], dict) + self.assertEqual(data['attach'][0]['fmttype'], 'text/html') + + self.assertIsInstance(data['rrule'], dict) + self.assertEqual(data['rrule']['frequency'], 'DAILY') + self.assertEqual(data['rrule']['interval'], 1) + self.assertEqual(data['rrule']['wkst'], 'MO') + self.assertIsInstance(data['rrule']['until'], datetime.date) + + self.assertIsInstance(data['alarm'], list) + self.assertEqual(len(data['alarm']), 2) + self.assertEqual(data['alarm'][0]['action'], 'DISPLAY') + self.assertEqual(data['alarm'][1]['action'], 'EMAIL') + self.assertEqual(data['alarm'][1]['trigger']['value'], '-P1D') + self.assertEqual(len(data['alarm'][1]['attendee']), 1) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py new file mode 100644 index 0000000..a08d05f --- /dev/null +++ b/tests/unit/test-011-itip.py @@ -0,0 +1,412 @@ +import pykolab +import datetime +import pytz +import kolabformat + +from pykolab import itip +from pykolab.xml import Event +from pykolab.xml import participant_status_label +from pykolab.translate import _ + +from icalendar import Calendar +from email import message +from email import message_from_string +from wallace import module_resources +from twisted.trial import unittest + +# define some iTip MIME messages + +itip_multipart = """MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_c8894dbdb8baeedacae836230e3436fd" +From: "Doe, John" <john.doe@example.org> +Date: Fri, 13 Jul 2012 13:54:14 +0100 +Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org> +User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 +To: resource-collection-car@example.org +Subject: "test" has been updated + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: quoted-printable + +*test* + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: text/calendar; charset=UTF-8; method=REQUEST; + name=event.ics +Content-Disposition: attachment; + filename=event.ics +Content-Transfer-Encoding: quoted-printable + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 +DTSTAMP:20120713T1254140 +DTSTART;TZID=3DEurope/London:20120713T100000 +DTEND;TZID=3DEurope/London:20120713T110000 +SUMMARY:test +DESCRIPTION:test +ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt= +o:resource-collection-car@example.org +ATTENDEE;ROLE=3DOPT-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:anoth= +er-resource@example.org +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + +--=_c8894dbdb8baeedacae836230e3436fd-- +""" + +itip_non_multipart = """Return-Path: <john.doe@example.org> +Sender: john.doe@example.org +Content-Type: text/calendar; method=REQUEST; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable +To: resource-collection-car@example.org +From: john.doe@example.org +Date: Mon, 24 Feb 2014 11:27:28 +0100 +Message-ID: <1a3aa8995e83dd24cf9247e538ac913a@example.org> +Subject: test + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 +DTSTAMP:20120713T1254140 +DTSTART;TZID=3DEurope/London:20120713T100000 +DTEND;TZID=3DEurope/London:20120713T110000 +SUMMARY:test +DESCRIPTION:test +ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSVP=3DTRUE:mailt= +o:resource-collection-car@example.org +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR +""" + +itip_google_multipart = """MIME-Version: 1.0 +Message-ID: <001a11c2ad84243e0604f3246bae@google.com> +Date: Mon, 24 Feb 2014 10:27:28 +0000 +Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?= + =?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?= +From: "john.doe" <john.doe@gmail.com> +To: <john.sample@example.org> +Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad + +--001a11c2ad84243df004f3246bad +Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab + +--001a11c2ad84243dec04f3246bab +Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes + +<some text content here> + +--001a11c2ad84243dec04f3246bab +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + +<div style=3D""><!-- some HTML message content here --></div> +--001a11c2ad84243dec04f3246bab +Content-Type: text/calendar; charset=UTF-8; method=REQUEST +Content-Transfer-Encoding: 7bit + +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +DTSTART:20140224T110000Z +DTEND:20140224T120000Z +DTSTAMP:20140224T102728Z +ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com +UID:0BE2F640-5814-47C9-ABAE-E7E959204E76 +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE + ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample@example.org +CREATED:20140224T102728Z +DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog + le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M + WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N + TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz + NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en + . +LAST-MODIFIED:20140224T102728Z +LOCATION: +SEQUENCE:5 +STATUS:CONFIRMED +SUMMARY:iTip from Apple +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + +--001a11c2ad84243dec04f3246bab-- +--001a11c2ad84243df004f3246bad +Content-Type: application/ics; name="invite.ics" +Content-Disposition: attachment; filename="invite.ics" +Content-Transfer-Encoding: base64 + +QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw +LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT +VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0 +VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq +bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF +MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ +RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7 +WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh +bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB +UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT +PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM +O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7 +Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls +LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp +cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j +b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3 +M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO +bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz +WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN +CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6 +PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN +CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy +b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg== +--001a11c2ad84243df004f3246bad-- +""" + +itip_application_ics = """MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_c8894dbdb8baeedacae836230e3436fd" +From: "Doe, John" <john.doe@example.org> +Date: Fri, 13 Jul 2012 13:54:14 +0100 +Message-ID: <240fe7ae7e139129e9eb95213c101622@example.org> +User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 +To: resource-collection-car@example.org +Subject: "test" has been updated + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8; format=flowed + +<some text here> + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: application/ics; charset=UTF-8; method=REQUEST; + name=event.ics +Content-Transfer-Encoding: quoted-printable + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 +DTSTAMP:20120713T1254140 +DTSTART;TZID=3DEurope/London:20120713T100000 +DTEND;TZID=3DEurope/London:20120713T110000 +SUMMARY:test +DESCRIPTION:test +ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt= +o:resource-collection-car@example.org +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + +--=_c8894dbdb8baeedacae836230e3436fd-- +""" + +itip_recurring = """Return-Path: <john.doe@example.org> +Sender: john.doe@example.org +Content-Type: text/calendar; method=REQUEST; charset=UTF-8 +Content-Transfer-Encoding: 8bit +From: john.doe@example.org +Date: Mon, 24 Feb 2014 11:27:28 +0100 +Message-ID: <1a3aa8995e83dd24cf9247e538ac913a@example.org> +Subject: Recurring + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:dbdb8baeedacae836230e3436fd-5e83dd24cf92 +DTSTAMP:20140213T1254140 +DTSTART;TZID=Europe/London:20120709T100000 +DTEND;TZID=Europe/London:20120709T120000 +RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5 +SUMMARY:Recurring +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:jane@example.com +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR +""" + +itip_empty = """MIME-Version: 1.0 +Date: Fri, 17 Jan 2014 13:51:50 +0100 +From: <john.doe@example.org> +User-Agent: Roundcube Webmail/0.9.5 +To: john.sample@example.org +Subject: "test" has been sent +Message-ID: <52D92766.5040508@somedomain.com> +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Message plain text goes here... +""" + +conf = pykolab.getConf() + +if not hasattr(conf, 'defaults'): + conf.finalize_conf() + +class TestITip(unittest.TestCase): + + def setUp(self): + # intercept calls to smtplib.SMTP.sendmail() + import smtplib + self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init) + self.patch(smtplib.SMTP, "quit", self._mock_nop) + self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail) + + self.smtplog = []; + + def _mock_nop(self, domain=None): + pass + + def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0): + pass + + def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None): + self.smtplog.append((from_addr, to_addr, message)) + + + def test_001_itip_events_from_message(self): + itips1 = itip.events_from_message(message_from_string(itip_multipart)) + self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar") + self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property") + + itips2 = itip.events_from_message(message_from_string(itip_non_multipart)) + self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages") + + itips3 = itip.events_from_message(message_from_string(itip_application_ics)) + self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment") + + itips4 = itip.events_from_message(message_from_string(itip_google_multipart)) + self.assertEqual(len(itips4), 1, "Multipart iTip message from Google") + + itips5 = itip.events_from_message(message_from_string(itip_empty)) + self.assertEqual(len(itips5), 0, "Simple plain text message") + + # invalid itip blocks + self.assertRaises(Exception, itip.events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", ""))) + + itips6 = itip.events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;"))) + self.assertEqual(len(itips6), 0, "Event with not DTSTART") + + itips7 = itip.events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH"))) + self.assertEqual(len(itips7), 0, "Invalid METHOD") + + + def test_002_check_date_conflict(self): + astart = datetime.datetime(2014,7,13, 10,0,0) + aend = astart + datetime.timedelta(hours=2) + + bstart = datetime.datetime(2014,7,13, 10,0,0) + bend = astart + datetime.timedelta(hours=1) + self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend)) + + bstart = datetime.datetime(2014,7,13, 11,0,0) + bend = astart + datetime.timedelta(minutes=30) + self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend)) + + bend = astart + datetime.timedelta(hours=2) + self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend)) + + bstart = datetime.datetime(2014,7,13, 12,0,0) + bend = astart + datetime.timedelta(hours=1) + self.assertFalse(itip.check_date_conflict(astart, aend, bstart, bend)) + + bstart = datetime.datetime(2014,6,13, 10,0,0) + bend = datetime.datetime(2014,6,14, 12,0,0) + self.assertFalse(itip.check_date_conflict(astart, aend, bstart, bend)) + + bstart = datetime.datetime(2014,7,10, 12,0,0) + bend = datetime.datetime(2014,7,14, 14,0,0) + self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend)) + + + def test_002_check_event_conflict(self): + itip_event = itip.events_from_message(message_from_string(itip_non_multipart))[0] + + event = Event() + event.set_start(datetime.datetime(2012,7,13, 9,30,0, tzinfo=itip_event['start'].tzinfo)) + event.set_end(datetime.datetime(2012,7,13, 10,30,0, tzinfo=itip_event['start'].tzinfo)) + + self.assertTrue(itip.check_event_conflict(event, itip_event), "Conflicting dates") + + event.set_uid(itip_event['uid']) + self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict for same UID") + + allday = Event() + allday.set_start(datetime.date(2012,7,13)) + allday.set_end(datetime.date(2012,7,13)) + + self.assertTrue(itip.check_event_conflict(allday, itip_event), "Conflicting allday event") + + allday.set_transparency(True) + self.assertFalse(itip.check_event_conflict(allday, itip_event), "No conflict if event is set to transparent") + + event2 = Event() + event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central"))) + event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central"))) + + self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict with timezone shift") + + rrule = kolabformat.RecurrenceRule() + rrule.setFrequency(kolabformat.RecurrenceRule.Weekly) + rrule.setCount(10) + + event3 = Event() + event3.set_recurrence(rrule); + event3.set_start(datetime.datetime(2012,6,29, 9,30,0, tzinfo=pytz.utc)) + event3.set_end(datetime.datetime(2012,6,29, 10,30,0, tzinfo=pytz.utc)) + + self.assertTrue(itip.check_event_conflict(event3, itip_event), "Conflict in (3rd) recurring event instance") + + itip_event = itip.events_from_message(message_from_string(itip_recurring))[0] + self.assertTrue(itip.check_event_conflict(event3, itip_event), "Conflict in two recurring events") + + event4 = Event() + event4.set_recurrence(rrule); + event4.set_start(datetime.datetime(2012,7,1, 9,30,0, tzinfo=pytz.utc)) + event4.set_end(datetime.datetime(2012,7,1, 10,30,0, tzinfo=pytz.utc)) + self.assertFalse(itip.check_event_conflict(event4, itip_event), "No conflict in two recurring events") + + + def test_003_send_reply(self): + itip_events = itip.events_from_message(message_from_string(itip_non_multipart)) + itip.send_reply("resource-collection-car@example.org", itip_events, "SUMMARY=%(summary)s; STATUS=%(status)s; NAME=%(name)s;") + + self.assertEqual(len(self.smtplog), 1) + self.assertEqual(self.smtplog[0][0], 'resource-collection-car@example.org', "From attendee") + self.assertEqual(self.smtplog[0][1], 'john.doe@example.org', "To organizer") + + _accepted = participant_status_label('ACCEPTED') + message = message_from_string(self.smtplog[0][2]) + self.assertEqual(message.get('Subject'), _("Invitation for %(summary)s was %(status)s") % { 'summary':'test', 'status':_accepted }) + + text = str(message.get_payload(0)); + self.assertIn('SUMMARY=test', text) + self.assertIn('STATUS=' + _accepted, text) diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py index 62bfd27..9c42317 100644 --- a/tests/unit/test-011-wallace_resources.py +++ b/tests/unit/test-011-wallace_resources.py @@ -2,6 +2,7 @@ import pykolab import logging import datetime +from pykolab import itip from icalendar import Calendar from email import message from email import message_from_string @@ -87,152 +88,6 @@ END:VEVENT END:VCALENDAR """ -itip_google_multipart = """MIME-Version: 1.0 -Message-ID: <001a11c2ad84243e0604f3246bae@google.com> -Date: Mon, 24 Feb 2014 10:27:28 +0000 -Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?= - =?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?= -From: "john.doe" <john.doe@gmail.com> -To: <john.sample@example.org> -Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad - ---001a11c2ad84243df004f3246bad -Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab - ---001a11c2ad84243dec04f3246bab -Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes - -<some text content here> - ---001a11c2ad84243dec04f3246bab -Content-Type: text/html; charset=ISO-8859-1 -Content-Transfer-Encoding: quoted-printable - -<div style=3D""><!-- some HTML message content here --></div> ---001a11c2ad84243dec04f3246bab -Content-Type: text/calendar; charset=UTF-8; method=REQUEST -Content-Transfer-Encoding: 7bit - -BEGIN:VCALENDAR -PRODID:-//Google Inc//Google Calendar 70.9054//EN -VERSION:2.0 -CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VEVENT -DTSTART:20140224T110000Z -DTEND:20140224T120000Z -DTSTAMP:20140224T102728Z -ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com -UID:0BE2F640-5814-47C9-ABAE-E7E959204E76 -ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE - ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com -ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= - TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample@example.org -CREATED:20140224T102728Z -DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog - le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M - WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N - TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz - NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en - . -LAST-MODIFIED:20140224T102728Z -LOCATION: -SEQUENCE:5 -STATUS:CONFIRMED -SUMMARY:iTip from Apple -TRANSP:OPAQUE -END:VEVENT -END:VCALENDAR - ---001a11c2ad84243dec04f3246bab-- ---001a11c2ad84243df004f3246bad -Content-Type: application/ics; name="invite.ics" -Content-Disposition: attachment; filename="invite.ics" -Content-Transfer-Encoding: base64 - -QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw -LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT -VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0 -VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq -bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF -MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ -RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7 -WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh -bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB -UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT -PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM -O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7 -Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls -LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp -cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j -b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3 -M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO -bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz -WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN -CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6 -PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN -CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy -b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg== ---001a11c2ad84243df004f3246bad-- -""" - -itip_application_ics = """MIME-Version: 1.0 -Content-Type: multipart/mixed; - boundary="=_c8894dbdb8baeedacae836230e3436fd" -From: "Doe, John" <john.doe@example.org> -Date: Fri, 13 Jul 2012 13:54:14 +0100 -Message-ID: <240fe7ae7e139129e9eb95213c101622@example.org> -User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 -To: resource-collection-car@example.org -Subject: "test" has been updated - ---=_c8894dbdb8baeedacae836230e3436fd -Content-Transfer-Encoding: quoted-printable -Content-Type: text/plain; charset=UTF-8; format=flowed - -<some text here> - ---=_c8894dbdb8baeedacae836230e3436fd -Content-Type: application/ics; charset=UTF-8; method=REQUEST; - name=event.ics -Content-Transfer-Encoding: quoted-printable - -BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN -CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VEVENT -UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 -DTSTAMP:20120713T1254140 -DTSTART;TZID=3DEurope/London:20120713T100000 -DTEND;TZID=3DEurope/London:20120713T110000 -SUMMARY:test -DESCRIPTION:test -ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org -ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt= -o:resource-collection-car@example.org -TRANSP:OPAQUE -END:VEVENT -END:VCALENDAR - ---=_c8894dbdb8baeedacae836230e3436fd-- -""" - -itip_empty = """MIME-Version: 1.0 -Date: Fri, 17 Jan 2014 13:51:50 +0100 -From: <john.doe@example.org> -User-Agent: Roundcube Webmail/0.9.5 -To: john.sample@example.org -Subject: "test" has been sent -Message-ID: <52D92766.5040508@somedomain.com> -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 7bit - -Message plain text goes here... -""" - - conf = pykolab.getConf() if not hasattr(conf, 'defaults'): @@ -272,7 +127,7 @@ class TestWallaceResources(unittest.TestCase): def _mock_search_entry_by_attribute(self, attr, value, **kw): results = [] if value == "cn=Room 101,ou=Resources,dc=example,dc=org": - results.append({ 'dn': 'cn=Rooms,ou=Resources,dc=example,dc=org', attr: value, 'owner': 'uid=doe,ou=People,dc=example,dc=org' }) + results.append(('cn=Rooms,ou=Resources,dc=example,dc=org', { attr: value, 'owner': 'uid=doe,ou=People,dc=example,dc=org' })) return results def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0): @@ -301,32 +156,6 @@ class TestWallaceResources(unittest.TestCase): return None - def test_001_itip_events_from_message(self): - itips1 = module_resources.itip_events_from_message(message_from_string(itip_multipart)) - self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar") - self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property") - - itips2 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart)) - self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages") - - itips3 = module_resources.itip_events_from_message(message_from_string(itip_application_ics)) - self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment") - - itips4 = module_resources.itip_events_from_message(message_from_string(itip_google_multipart)) - self.assertEqual(len(itips4), 1, "Multipart iTip message from Google") - - itips5 = module_resources.itip_events_from_message(message_from_string(itip_empty)) - self.assertEqual(len(itips5), 0, "Simple plain text message") - - # invalid itip blocks - self.assertRaises(Exception, module_resources.itip_events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", ""))) - - itips6 = module_resources.itip_events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;"))) - self.assertEqual(len(itips6), 0, "Event with not DTSTART") - - itips7 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH"))) - self.assertEqual(len(itips7), 0, "Invalid METHOD") - def test_002_resource_record_from_email_address(self): res = module_resources.resource_record_from_email_address("doe@example.org") @@ -337,7 +166,7 @@ class TestWallaceResources(unittest.TestCase): def test_003_resource_records_from_itip_events(self): message = message_from_string(itip_multipart) - itips = module_resources.itip_events_from_message(message) + itips = itip.events_from_message(message) res = module_resources.resource_records_from_itip_events(itips) self.assertEqual(len(res), 2, "Return all attendee resources"); @@ -365,7 +194,7 @@ class TestWallaceResources(unittest.TestCase): def test_005_send_response_accept(self): - itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart)) + itip_event = itip.events_from_message(message_from_string(itip_non_multipart)) module_resources.send_response("resource-collection-car@example.org", itip_event) self.assertEqual(len(self.smtplog), 1); @@ -384,7 +213,7 @@ class TestWallaceResources(unittest.TestCase): def test_006_send_response_delegate(self): # delegate resource-collection-car@example.org => resource-car-audi-a4@example.org - itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))[0] + itip_event = itip.events_from_message(message_from_string(itip_non_multipart))[0] itip_event['xml'].delegate('resource-collection-car@example.org', 'resource-car-audi-a4@example.org') itip_event['xml'].set_attendee_participant_status(itip_event['xml'].get_attendee('resource-car-audi-a4@example.org'), "ACCEPTED") @@ -408,30 +237,3 @@ class TestWallaceResources(unittest.TestCase): self.assertEqual(ical2['attendee'].params['PARTSTAT'], "DELEGATED") - def test_007_check_date_conflict(self): - astart = datetime.datetime(2014,7,13, 10,0,0) - aend = astart + datetime.timedelta(hours=2) - - bstart = datetime.datetime(2014,7,13, 10,0,0) - bend = astart + datetime.timedelta(hours=1) - self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend)) - - bstart = datetime.datetime(2014,7,13, 11,0,0) - bend = astart + datetime.timedelta(minutes=30) - self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend)) - - bend = astart + datetime.timedelta(hours=2) - self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend)) - - bstart = datetime.datetime(2014,7,13, 12,0,0) - bend = astart + datetime.timedelta(hours=1) - self.assertFalse(module_resources.check_date_conflict(astart, aend, bstart, bend)) - - bstart = datetime.datetime(2014,6,13, 10,0,0) - bend = datetime.datetime(2014,6,14, 12,0,0) - self.assertFalse(module_resources.check_date_conflict(astart, aend, bstart, bend)) - - bstart = datetime.datetime(2014,7,10, 12,0,0) - bend = datetime.datetime(2014,7,14, 14,0,0) - self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend)) - diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py new file mode 100644 index 0000000..dbe0713 --- /dev/null +++ b/tests/unit/test-012-wallace_invitationpolicy.py @@ -0,0 +1,161 @@ +import os +import pykolab +import logging +import time + +from icalendar import Calendar +from email import message +from email import message_from_string +from wallace import module_invitationpolicy as MIP +from twisted.trial import unittest + +from pykolab.auth.ldap import LDAP +from pykolab.constants import * + + +# define some iTip MIME messages + +itip_multipart = """MIME-Version: 1.0 +Content-Type: multipart/mixed; + boundary="=_c8894dbdb8baeedacae836230e3436fd" +From: "Doe, John" <john.doe@example.org> +Date: Fri, 13 Jul 2012 13:54:14 +0100 +Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org> +User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 +To: jane.doe@example.org +Subject: "test" has been updated + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: quoted-printable + +*test* + +--=_c8894dbdb8baeedacae836230e3436fd +Content-Type: text/calendar; charset=UTF-8; method=REQUEST; + name=event.ics +Content-Disposition: attachment; + filename=event.ics +Content-Transfer-Encoding: quoted-printable + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube Webmail 1.0.1//NONSGML Calendar//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 +DTSTAMP:20120713T125414Z +DTSTART;TZID=3DEurope/London:20120713T100000 +DTEND;TZID=3DEurope/London:20120713T110000 +SUMMARY:test +DESCRIPTION:test +ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt= +o:jane.doe@example.org +ATTENDEE;ROLE=3DOPT-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt= +user.external@example.com +SEQUENCE:1 +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + +--=_c8894dbdb8baeedacae836230e3436fd-- +""" + +conf = pykolab.getConf() + +if not hasattr(conf, 'defaults'): + conf.finalize_conf() + +class TestWallaceInvitationpolicy(unittest.TestCase): + + def setUp(self): + # monkey-patch the pykolab.auth module to check API calls + # without actually connecting to LDAP + self.patch(pykolab.auth.Auth, "connect", self._mock_nop) + self.patch(pykolab.auth.Auth, "disconnect", self._mock_nop) + self.patch(pykolab.auth.Auth, "find_user_dn", self._mock_find_user_dn) + self.patch(pykolab.auth.Auth, "get_entry_attributes", self._mock_get_entry_attributes) + + # intercept calls to smtplib.SMTP.sendmail() + import smtplib + self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init) + self.patch(smtplib.SMTP, "quit", self._mock_nop) + self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail) + + self.smtplog = []; + + def _mock_find_user_dn(self, value, kolabuser=False): + (prefix, domain) = value.split('@') + return "uid=" + prefix + ",ou=People,dc=" + ",dc=".join(domain.split('.')) + + def _mock_get_entry_attributes(self, domain, entry, attributes): + (_, uid) = entry.split(',')[0].split('=') + return { 'cn': uid, 'mail': uid + "@example.org", '_attrib': attributes } + + def _mock_nop(self, domain=None): + pass + + def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0): + pass + + def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None): + self.smtplog.append((from_addr, to_addr, message)) + + def test_001_itip_events_from_message(self): + itips = pykolab.itip.events_from_message(message_from_string(itip_multipart)) + self.assertEqual(len(itips), 1, "Multipart iTip message with text/calendar") + self.assertEqual(itips[0]['method'], "REQUEST", "iTip request method property") + self.assertEqual(len(itips[0]['attendees']), 2, "List attendees from iTip") + self.assertEqual(itips[0]['attendees'][0], "mailto:jane.doe@example.org", "First attendee from iTip") + + def test_002_user_dn_from_email_address(self): + res = MIP.user_dn_from_email_address("doe@example.org") + # assert call to (patched) pykolab.auth.Auth.find_resource() + self.assertEqual("uid=doe,ou=People,dc=example,dc=org", res); + + def test_003_get_matching_invitation_policy(self): + user = { 'kolabinvitationpolicy': [ + 'ACT_ACCEPT:example.org', + 'ACT_REJECT:gmail.com', + 'ACT_MANUAL:*' + ] } + self.assertEqual(MIP.get_matching_invitation_policies(user, 'fastmail.net'), [MIP.ACT_MANUAL]) + self.assertEqual(MIP.get_matching_invitation_policies(user, 'example.org'), [MIP.ACT_ACCEPT,MIP.ACT_MANUAL]) + self.assertEqual(MIP.get_matching_invitation_policies(user, 'gmail.com'), [MIP.ACT_REJECT,MIP.ACT_MANUAL]) + + user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] } + self.assertEqual(MIP.get_matching_invitation_policies(user, 'somedomain.net'), [MIP.ACT_MANUAL]) + + def test_004_write_locks(self): + user = { 'cn': 'John Doe', 'mail': "doe@example.org" } + + lock_key = MIP.get_lock_key(user, '1234567890-abcdef') + lock_file = os.path.join(MIP.mybasepath, 'locks', lock_key + '.lock') + MIP.set_write_lock(lock_key) + + time.sleep(1) + self.assertTrue(os.path.isfile(lock_file)) + self.assertFalse(MIP.set_write_lock(lock_key, False)) + + MIP.remove_write_lock(lock_key) + self.assertFalse(os.path.isfile(lock_file)) + + def test_005_is_auto_reply(self): + all_manual = [ 'ACT_MANUAL' ] + accept_none = [ 'ACT_REJECT' ] + accept_all = [ 'ACT_ACCEPT', 'ACT_UPDATE' ] + accept_cond = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT' ] + accept_some = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_SAVE_TO_CALENDAR:example.org', 'ACT_REJECT_IF_CONFLICT' ] + accept_avail = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT:example.org' ] + + self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual }, 'domain.org')) + self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'domain.org')) + self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'domain.com')) + self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'domain.com')) + self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'domain.com')) + self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'example.org')) + self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'domain.com')) + self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'example.org')) +
\ No newline at end of file diff --git a/tests/unit/test-015-translate.py b/tests/unit/test-015-translate.py new file mode 100644 index 0000000..6819b80 --- /dev/null +++ b/tests/unit/test-015-translate.py @@ -0,0 +1,23 @@ +import unittest +import gettext +from pykolab import translate + +class TestTranslate(unittest.TestCase): + + def test_001_default_langs(self): + self.assertTrue(len(translate.getDefaultLangs()) > 0) + + def test_002_translate(self): + from pykolab.translate import _ + self.assertEqual(_("Folder name"), "Folder name") + + def test_003_set_lang(self): + from pykolab.translate import _ + self.assertEqual(_("Folder name"), "Folder name") + translate.setUserLanguage('de_DE') + self.assertEqual(_("Folder name"), "Ordnername", "German Translation found") + translate.setUserLanguage('foo_bar') + self.assertEqual(_("Folder name"), "Folder name", "Unkonwn language falls back to NullTranslations") + +if __name__ == '__main__': + unittest.main() diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py new file mode 100644 index 0000000..9e0fa37 --- /dev/null +++ b/wallace/module_invitationpolicy.py @@ -0,0 +1,1015 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) +# +# Thomas Bruederli (Kolab Systems) <bruederli@kolabsys.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import datetime +import os +import tempfile +import time +from urlparse import urlparse +import urllib +import hashlib + +from email import message_from_string +from email.parser import Parser +from email.utils import formataddr +from email.utils import getaddresses + +import modules + +import pykolab +import kolabformat + +from pykolab import utils +from pykolab.auth import Auth +from pykolab.conf import Conf +from pykolab.imap import IMAP +from pykolab.xml import to_dt +from pykolab.xml import event_from_message +from pykolab.xml import participant_status_label +from pykolab.itip import events_from_message +from pykolab.itip import check_event_conflict +from pykolab.itip import send_reply +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 + +FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type' + +MESSAGE_PROCESSED = 1 +MESSAGE_FORWARD = 2 + +policy_name_map = { + 'ACT_MANUAL': ACT_MANUAL, + 'ACT_ACCEPT': ACT_ACCEPT, + 'ACT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT_IF_NO_CONFLICT, + 'ACT_TENTATIVE': ACT_TENTATIVE, + 'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_TENTATIVE_IF_NO_CONFLICT, + 'ACT_DELEGATE': ACT_DELEGATE, + 'ACT_DELEGATE_IF_CONFLICT': ACT_DELEGATE_IF_CONFLICT, + 'ACT_REJECT': ACT_REJECT, + 'ACT_REJECT_IF_CONFLICT': ACT_REJECT_IF_CONFLICT, + 'ACT_UPDATE': ACT_UPDATE, + 'ACT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY, + 'ACT_SAVE_TO_CALENDAR': ACT_SAVE_TO_CALENDAR +} + +policy_value_map = dict([(v, k) for (k, v) in policy_name_map.iteritems()]) + +log = pykolab.getLogger('pykolab.wallace') +conf = pykolab.getConf() + +mybasepath = '/var/spool/pykolab/wallace/invitationpolicy/' + +auth = None +imap = None +write_locks = [] + +def __init__(): + modules.register('invitationpolicy', execute, description=description()) + +def accept(filepath): + new_filepath = os.path.join( + mybasepath, + 'ACCEPT', + os.path.basename(filepath) + ) + + cleanup() + os.rename(filepath, new_filepath) + filepath = new_filepath + exec('modules.cb_action_ACCEPT(%r, %r)' % ('invitationpolicy',filepath)) + +def reject(filepath): + new_filepath = os.path.join( + mybasepath, + 'REJECT', + os.path.basename(filepath) + ) + + os.rename(filepath, new_filepath) + filepath = new_filepath + exec('modules.cb_action_REJECT(%r, %r)' % ('invitationpolicy',filepath)) + +def description(): + return """Invitation policy execution module.""" + +def cleanup(): + global auth, imap, write_locks + + log.debug("cleanup(): %r, %r" % (auth, imap), level=9) + + auth.disconnect() + del auth + + # Disconnect IMAP or we lock the mailbox almost constantly + imap.disconnect() + del imap + + # remove remaining write locks + for key in write_locks: + remove_write_lock(key, False) + +def execute(*args, **kw): + global auth, imap + + # (re)set language to default + pykolab.translate.setUserLanguage(conf.get('kolab','default_locale')) + + if not os.path.isdir(mybasepath): + os.makedirs(mybasepath) + + for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER', 'locks']: + if not os.path.isdir(os.path.join(mybasepath, stage)): + os.makedirs(os.path.join(mybasepath, stage)) + + log.debug(_("Invitation policy called for %r, %r") % (args, kw), level=9) + + auth = Auth() + imap = IMAP() + + filepath = args[0] + + # ignore calls on lock files + if '/locks/' in filepath or kw.has_key('stage') and kw['stage'] == 'locks': + return False + + log.debug("Invitation policy executing for %r, %r" % (filepath, '/locks/' in filepath), level=8) + + if kw.has_key('stage'): + log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) + + log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) + if hasattr(modules, 'cb_action_%s' % (kw['stage'])): + log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) + + exec( + 'modules.cb_action_%s(%r, %r)' % ( + kw['stage'], + 'invitationpolicy', + filepath + ) + ) + + return filepath + else: + # Move to incoming + new_filepath = os.path.join( + mybasepath, + 'incoming', + os.path.basename(filepath) + ) + + if not filepath == new_filepath: + log.debug("Renaming %r to %r" % (filepath, new_filepath)) + os.rename(filepath, new_filepath) + filepath = new_filepath + + # parse full message + message = Parser().parse(open(filepath, 'r')) + + recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))] + sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0] + + any_itips = False + recipient_email = None + recipient_user_dn = None + + # An iTip message may contain multiple events. Later on, test if the message + # is an iTip message by checking the length of this list. + try: + itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL']) + except Exception, e: + log.error(_("Failed to parse iTip events from message: %r" % (e))) + itip_events = [] + + if not len(itip_events) > 0: + log.info(_("Message is not an iTip message or does not contain any (valid) iTip events.")) + + else: + any_itips = True + log.debug(_("iTip events attached to this message contain the following information: %r") % (itip_events), level=9) + + # See if any iTip actually allocates a user. + if any_itips and len([x['uid'] for x in itip_events if x.has_key('attendees') or x.has_key('organizer')]) > 0: + auth.connect() + + for recipient in recipients: + recipient_user_dn = user_dn_from_email_address(recipient) + if recipient_user_dn: + recipient_email = recipient + break + + if not any_itips: + log.debug(_("No itips, no users, pass along %r") % (filepath), level=5) + return filepath + elif recipient_email is None: + log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5) + return filepath + + # we're looking at the first itip event object + itip_event = itip_events[0] + + # for replies, the organizer is the recipient + if itip_event['method'] == 'REPLY': + user_attendees = [itip_event['organizer']] if str(itip_event['organizer']).split(':')[-1] == recipient_email else [] + + else: + # Limit the attendees to the one that is actually invited with the current message. + attendees = [str(a).split(':')[-1] for a in (itip_event['attendees'] if itip_event.has_key('attendees') else [])] + user_attendees = [a for a in attendees if a == recipient_email] + + if itip_event.has_key('organizer'): + sender_email = itip_event['xml'].get_organizer().email() + + # abort if no attendee matches the envelope recipient + if len(user_attendees) == 0: + log.info(_("No user attendee matching envelope recipient %s, skip message") % (recipient_email)) + return filepath + + receiving_user = auth.get_entry_attributes(None, recipient_user_dn, ['*']) + log.debug(_("Receiving user: %r") % (receiving_user), level=8) + + # change gettext language to the preferredlanguage setting of the receiving user + if receiving_user.has_key('preferredlanguage'): + pykolab.translate.setUserLanguage(receiving_user['preferredlanguage']) + + # find user's kolabInvitationPolicy settings and the matching policy values + sender_domain = str(sender_email).split('@')[-1] + policies = get_matching_invitation_policies(receiving_user, sender_domain) + + # select a processing function according to the iTip request method + method_processing_map = { + 'REQUEST': process_itip_request, + 'REPLY': process_itip_reply, + 'CANCEL': process_itip_cancel + } + + done = None + if method_processing_map.has_key(itip_event['method']): + processor_func = method_processing_map[itip_event['method']] + + # connect as cyrus-admin + imap.connect() + + for policy in policies: + log.debug(_("Apply invitation policy %r for domain %r") % (policy_value_map[policy], sender_domain), level=8) + done = processor_func(itip_event, policy, recipient_email, sender_email, receiving_user) + + # matching policy found + if done is not None: + break + + # remove possible write lock from this iteration + remove_write_lock(get_lock_key(receiving_user, itip_event['uid'])) + + else: + log.debug(_("Ignoring '%s' iTip method") % (itip_event['method']), level=8) + + # message has been processed by the module, remove it + if done == MESSAGE_PROCESSED: + log.debug(_("iTip message %r consumed by the invitationpolicy module") % (message.get('Message-ID')), level=5) + os.unlink(filepath) + cleanup() + return None + + # accept message into the destination inbox + accept(filepath) + + +def process_itip_request(itip_event, policy, recipient_email, sender_email, receiving_user): + """ + Process an iTip REQUEST message according to the given policy + """ + + # if invitation policy is set to MANUAL, pass message along + if policy & ACT_MANUAL: + log.info(_("Pass invitation for manual processing")) + return MESSAGE_FORWARD + + try: + receiving_attendee = itip_event['xml'].get_attendee_by_email(recipient_email) + log.debug(_("Receiving Attendee: %r") % (receiving_attendee), level=9) + except Exception, e: + log.error("Could not find envelope attendee: %r" % (e)) + return MESSAGE_FORWARD + + # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION + nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant + partstat = receiving_attendee.get_participant_status() + save_event = not nonpart or not partstat == kolabformat.PartNeedsAction + 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) + + # compare sequence number to determine a (re-)scheduling request + if existing is not None: + log.debug(_("Existing event: %r") % (existing), level=9) + scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence() + save_event = True + + # if scheduling: check availability + if scheduling_required: + if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): + condition_fulfilled = check_availability(itip_event, receiving_user) + if policy & COND_IF_CONFLICT: + condition_fulfilled = not condition_fulfilled + + log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5) + + respond_with = None + if policy & ACT_ACCEPT and condition_fulfilled: + respond_with = 'TENTATIVE' if policy & COND_TENTATIVE else 'ACCEPTED' + + elif policy & ACT_REJECT and condition_fulfilled: + respond_with = 'DECLINED' + # TODO: only save declined invitation when a certain config option is set? + + elif policy & ACT_DELEGATE and condition_fulfilled: + # TODO: delegate (but to whom?) + return None + + # if RSVP, send an iTip REPLY + if rsvp or scheduling_required: + # set attendee's CN from LDAP record if yet missing + if not receiving_attendee.get_name() and receiving_user.has_key('cn'): + receiving_attendee.set_name(receiving_user['cn']) + + # send iTip reply + if respond_with is not None: + receiving_attendee.set_participant_status(respond_with) + send_reply(recipient_email, itip_event, invitation_response_text(), + 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 + itip_event['xml'].set_attendee_participant_status(receiving_attendee, 'NEEDS-ACTION') + save_event = 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: + targetfolder = None + + if existing: + # delete old version from IMAP + targetfolder = existing._imap_folder + delete_event(existing) + + if not nonpart or existing: + # save new copy from iTip + if store_event(itip_event['xml'], receiving_user, targetfolder): + return MESSAGE_PROCESSED + + return None + + +def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiving_user): + """ + Process an iTip REPLY message according to the given policy + """ + + # if invitation policy is set to MANUAL, pass message along + if policy & ACT_MANUAL: + log.info(_("Pass reply for manual processing")) + return MESSAGE_FORWARD + + # auto-update is enabled for this user + if policy & ACT_UPDATE: + try: + sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email) + log.debug(_("Sender Attendee: %r") % (sender_attendee), level=9) + except Exception, e: + log.error("Could not find envelope sender attendee: %r" % (e)) + return MESSAGE_FORWARD + + # find existing event in user's calendar + # TODO: set/check lock to avoid concurrent wallace processes trying to update the same event simultaneously + existing = find_existing_event(itip_event['uid'], receiving_user, 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.") % ( + 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) + try: + 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)) + + # TODO: accept new participant if ACT_ACCEPT ? + remove_write_lock(existing._lock_key) + return MESSAGE_FORWARD + + # update the organizer's copy of the event + if update_event(existing, receiving_user): + if policy & COND_NOTIFY: + send_reply_notification(existing, receiving_user) + + # update all other attendee's copies + if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): + propagate_changes_to_attendees_calendars(existing) + + return MESSAGE_PROCESSED + + else: + log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox.")) + return MESSAGE_FORWARD + + return None + + +def process_itip_cancel(itip_event, policy, recipient_email, sender_email, receiving_user): + """ + Process an iTip CANCEL message according to the given policy + """ + + # if invitation policy is set to MANUAL, pass message along + if policy & ACT_MANUAL: + log.info(_("Pass cancellation for manual processing")) + return MESSAGE_FORWARD + + # 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) + + if existing: + existing.set_status('CANCELLED') + existing.set_transparency(True) + if update_event(existing, receiving_user): + # TODO: send cancellation notification if policy & ACT_UPDATE_AND_NOTIFY: ? + return MESSAGE_PROCESSED + + else: + log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox.")) + return MESSAGE_FORWARD + + return None + + +def user_dn_from_email_address(email_address): + """ + Resolves the given email address to a Kolab user entity + """ + global auth + + if not auth: + auth = Auth() + auth.connect() + + # return cached value + if user_dn_from_email_address.cache.has_key(email_address): + return user_dn_from_email_address.cache[email_address] + + local_domains = auth.list_domains() + + if not local_domains == None: + local_domains = list(set(local_domains.keys())) + + if not email_address.split('@')[1] in local_domains: + user_dn_from_email_address.cache[email_address] = None + return None + + log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8) + + user_dn = auth.find_user_dn(email_address, True) + + if isinstance(user_dn, basestring): + log.debug(_("User DN: %r") % (user_dn), level=8) + else: + log.debug(_("No user record(s) found for %r") % (email_address), level=9) + + # remember this lookup + user_dn_from_email_address.cache[email_address] = user_dn + + return user_dn + +user_dn_from_email_address.cache = {} + + +def get_matching_invitation_policies(receiving_user, sender_domain): + # get user's kolabInvitationPolicy settings + policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else [] + if policies and not isinstance(policies, list): + policies = [policies] + + if len(policies) == 0: + policies = conf.get_list('wallace', 'kolab_invitation_policy') + + # match policies agains the given sender_domain + matches = [] + for p in policies: + if ':' in p: + (value, domain) = p.split(':') + else: + value = p + domain = '' + + if domain == '' or domain == '*' or sender_domain.endswith(domain): + value = value.upper() + if policy_name_map.has_key(value): + matches.append(policy_name_map[value]) + + # add manual as default action + if len(matches) == 0: + matches.append(ACT_MANUAL) + + return matches + + +def imap_proxy_auth(user_rec): + """ + + """ + global imap + + mail_attribute = conf.get('cyrus-sasl', 'result_attribute') + if mail_attribute == None: + mail_attribute = 'mail' + + mail_attribute = mail_attribute.lower() + + if not user_rec.has_key(mail_attribute): + log.error(_("User record doesn't have the mailbox attribute %r set" % (mail_attribute))) + return False + + # do IMAP prox auth with the given user + backend = conf.get('kolab', 'imap_backend') + admin_login = conf.get(backend, 'admin_login') + admin_password = conf.get(backend, 'admin_password') + + try: + imap.disconnect() + imap.connect(login=False) + imap.login_plain(admin_login, admin_password, user_rec[mail_attribute]) + except Exception, errmsg: + log.error(_("IMAP proxy authentication failed: %r") % (errmsg)) + return False + + return True + + +def list_user_calendars(user_rec): + """ + Get a list of the given user's private calendar folders + """ + global imap + + # return cached list + if user_rec.has_key('_calendar_folders'): + return user_rec['_calendar_folders']; + + calendars = [] + + if not imap_proxy_auth(user_rec): + return calendars + + folders = imap.list_folders('*') + log.debug(_("List calendar folders for user %r: %r") % (user_rec['mail'], folders), level=8) + + (ns_personal, ns_other, ns_shared) = imap.namespaces() + + if isinstance(ns_shared, list): + ns_shared = ns_shared[0] + if isinstance(ns_other, list): + ns_other = ns_other[0] + + for folder in folders: + # exclude shared and other user's namespace + # TODO: list shared folders the user has write privileges ? + if folder.startswith(ns_other) or folder.startswith(ns_shared): + continue; + + metadata = imap.get_metadata(folder) + log.debug(_("IMAP metadata for %r: %r") % (folder, metadata), level=9) + if metadata.has_key(folder) and ( \ + metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith('event') \ + or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith('event')): + calendars.append(folder) + + # store default calendar folder in user record + if metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].endswith('.default'): + user_rec['_default_calendar'] = folder + + # cache with user record + user_rec['_calendar_folders'] = calendars + + return calendars + + +def find_existing_event(uid, user_rec, lock=False): + """ + Search user's calendar folders for the given event (by UID) + """ + global imap + + lock_key = None + + if lock: + lock_key = get_lock_key(user_rec, uid) + set_write_lock(lock_key) + + event = None + for folder in list_user_calendars(user_rec): + log.debug(_("Searching folder %r for event %r") % (folder, uid), level=8) + imap.imap.m.select(imap.folder_utf7(folder)) + + typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) + for num in reversed(data[0].split()): + typ, data = imap.imap.m.fetch(num, '(RFC822)') + + try: + event = event_from_message(message_from_string(data[0][1])) + setattr(event, '_imap_folder', folder) + setattr(event, '_lock_key', lock_key) + except Exception, e: + log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e)) + continue + + if event and event.uid == uid: + return event + + if lock_key is not None: + remove_write_lock(lock_key) + + return event + + +def check_availability(itip_event, receiving_user): + """ + For the receiving user, determine if the event in question is in conflict. + """ + + start = time.time() + num_messages = 0 + conflict = False + + # return previously detected conflict + if itip_event.has_key('_conflicts'): + return not itip_event['_conflicts'] + + for folder in list_user_calendars(receiving_user): + log.debug(_("Listing events from folder %r") % (folder), level=8) + imap.imap.m.select(imap.folder_utf7(folder)) + + typ, data = imap.imap.m.search(None, '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")') + num_messages += len(data[0].split()) + + for num in reversed(data[0].split()): + event = None + typ, data = imap.imap.m.fetch(num, '(RFC822)') + + try: + event = event_from_message(message_from_string(data[0][1])) + except Exception, e: + log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e)) + continue + + if event and event.uid: + conflict = check_event_conflict(event, itip_event) + if conflict: + log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid'])) + break + + if conflict: + break + + end = time.time() + log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=9) + + # remember the result of this check for further iterations + itip_event['_conflicts'] = conflict + + return not conflict + + +def set_write_lock(key, wait=True): + """ + Set a write-lock for the given key and wait if such a lock already exists + """ + if not os.path.isdir(mybasepath): + os.makedirs(mybasepath) + if not os.path.isdir(os.path.join(mybasepath, 'locks')): + os.makedirs(os.path.join(mybasepath, 'locks')) + + file = os.path.join(mybasepath, 'locks', key + '.lock') + locked = os.path.getmtime(file) if os.path.isfile(file) else 0 + expired = time.time() - 300 + + # wait if file lock is in place + while locked and locked > expired: + if not wait: + return False + + log.debug(_("%r is locked, waiting...") % (key), level=9) + time.sleep(0.5) + locked = os.path.getmtime(file) if os.path.isfile(file) else 0 + + # touch the file + if os.path.isfile(file): + os.utime(file, None) + else: + open(file, 'w').close() + + # register active lock + write_locks.append(key) + + return True + + +def remove_write_lock(key, update=True): + """ + Remove the lock file for the given key + """ + global write_locks + + if key is not None: + file = os.path.join(mybasepath, 'locks', key + '.lock') + if os.path.isfile(file): + os.remove(file) + if update: + write_locks = [k for k in write_locks if not k == key] + + +def get_lock_key(user, uid): + return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest() + + +def update_event(event, user_rec): + """ + Update the given event in IMAP (i.e. delete + append) + """ + success = False + + if hasattr(event, '_imap_folder'): + delete_event(event) + success = store_event(event, user_rec, event._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) + + return success + + +def store_event(event, user_rec, targetfolder=None): + """ + Append the given event object to the user's default calendar + """ + + # find default calendar folder to save event to + if targetfolder is None: + targetfolder = list_user_calendars(user_rec)[0] + if user_rec.has_key('_default_calendar'): + targetfolder = user_rec['_default_calendar'] + + if not targetfolder: + log.error(_("Failed to save event: no calendar folder found for user %r") % (user_rec['mail'])) + return Fasle + + log.debug(_("Save event %r to user calendar %r") % (event.uid, targetfolder), level=8) + + try: + imap.imap.m.select(imap.folder_utf7(targetfolder)) + result = imap.imap.m.append( + imap.folder_utf7(targetfolder), + None, + None, + event.to_message().as_string() + ) + return result + + except Exception, e: + log.error(_("Failed to save event to user calendar at %r: %r") % ( + targetfolder, e + )) + + return False + + +def delete_event(existing): + """ + Removes the IMAP object with the given UID from a user's calendar folder + """ + targetfolder = existing._imap_folder + imap.imap.m.select(imap.folder_utf7(targetfolder)) + + typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid) + + log.debug(_("Delete event %r in %r: %r") % ( + existing.uid, targetfolder, data + ), level=8) + + for num in data[0].split(): + imap.imap.m.store(num, '+FLAGS', '\\Deleted') + + imap.imap.m.expunge() + + +def send_reply_notification(event, receiving_user): + """ + Send a (consolidated) notification about the current participant status to organizer + """ + global auth + + import smtplib + from email.MIMEText import MIMEText + from email.Utils import formatdate + + log.debug(_("Compose participation status summary for event %r to user %r") % ( + event.uid, receiving_user['mail'] + ), level=8) + + organizer = event.get_organizer() + orgemail = organizer.email() + orgname = organizer.name() + sender_domain = orgemail.split('@')[-1] + + auto_replies_expected = 0 + auto_replies_received = 0 + partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] } + for attendee in event.get_attendees(): + parstat = attendee.get_participant_status(True) + 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, sender_domain): + auto_replies_expected += 1 + if not parstat == 'NEEDS-ACTION': + auto_replies_received += 1 + + # skip notification until we got replies from all automatically responding attendees + if auto_replies_received < auto_replies_expected: + log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % ( + auto_replies_received, auto_replies_expected + ), level=8) + return + + roundup = '' + for status,attendees in partstats.iteritems(): + if len(attendees) > 0: + roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n" + + message_text = """ + The event '%(summary)s' at %(start)s has been updated in your calendar. + %(roundup)s + """ % { + 'summary': event.get_summary(), + 'start': event.get_start().strftime('%Y-%m-%d %H:%M %Z'), + 'roundup': roundup + } + + # compose 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['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail + + smtp = smtplib.SMTP("localhost", 10027) + + if conf.debuglevel > 8: + smtp.set_debuglevel(True) + + try: + smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string()) + except Exception, e: + log.error(_("SMTP sendmail error: %r") % (e)) + + smtp.quit() + + +def is_auto_reply(user, sender_domain): + accept_available = False + accept_conflicts = False + for policy in get_matching_invitation_policies(user, sender_domain): + if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE): + if check_policy_condition(policy, True): + accept_available = True + if check_policy_condition(policy, False): + accept_conflicts = True + + # we have both cases covered by a policy + if accept_available and accept_conflicts: + return True + + # manual action reached + if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR): + return False + + return False + + +def check_policy_condition(policy, available): + condition_fulfilled = True + if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): + condition_fulfilled = available + if policy & COND_IF_CONFLICT: + condition_fulfilled = not condition_fulfilled + return condition_fulfilled + + +def propagate_changes_to_attendees_calendars(event): + """ + Find and update copies of this event in all attendee's calendars + """ + for attendee in event.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: + try: + attendee_entry = attendee_event.get_attendee_by_email(attendee_user['mail']) + except: + attendee_entry = None + + # copy all attendees from master event (covers additions and removals) + new_attendees = kolabformat.vectorattendee(); + for a in event.get_attendees(): + # keep my own entry intact + if attendee_entry is not None and attendee_entry.get_email() == a.get_email(): + new_attendees.append(attendee_entry) + else: + new_attendees.append(a) + + attendee_event.event.setAttendees(new_attendees) + + success = update_event(attendee_event, attendee_user) + log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], event.uid, success), level=8) + + else: + log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], event.uid), level=8) + + else: + log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8) + + +def invitation_response_text(): + return _(""" + %(name)s has %(status)s your invitation for %(summary)s. + + *** This is an automated response sent by the Kolab Invitation system *** + """) diff --git a/wallace/module_resources.py b/wallace/module_resources.py index 303252b..47259da 100644 --- a/wallace/module_resources.py +++ b/wallace/module_resources.py @@ -25,7 +25,9 @@ import random import tempfile import time from urlparse import urlparse -import urllib +import base64 +import uuid +import re from email import message_from_string from email.parser import Parser @@ -40,11 +42,25 @@ import kolabformat from pykolab.auth import Auth from pykolab.conf import Conf from pykolab.imap import IMAP -from pykolab.xml import event_from_ical -from pykolab.xml import event_from_string from pykolab.xml import to_dt +from pykolab.xml import event_from_message +from pykolab.xml import participant_status_label +from pykolab.itip import events_from_message +from pykolab.itip import check_event_conflict from pykolab.translate import _ +# define some contstants used in the code below +COND_NOTIFY = 256 +ACT_MANUAL = 1 +ACT_ACCEPT = 2 +ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY + +policy_name_map = { + 'ACT_MANUAL': ACT_MANUAL, + 'ACT_ACCEPT': ACT_ACCEPT, + 'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY +} + log = pykolab.getLogger('pykolab.wallace') conf = pykolab.getConf() @@ -86,6 +102,9 @@ def cleanup(): def execute(*args, **kw): global auth, imap + # (re)set language to default + pykolab.translate.setUserLanguage(conf.get('kolab','default_locale')) + if not os.path.isdir(mybasepath): os.makedirs(mybasepath) @@ -142,15 +161,17 @@ def execute(*args, **kw): message = Parser().parse(open(filepath, 'r')) recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))] + sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0] any_itips = False any_resources = False possibly_any_resources = True + reference_uid = None # An iTip message may contain multiple events. Later on, test if the message # is an iTip message by checking the length of this list. try: - itip_events = itip_events_from_message(message) + itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL']) except Exception, e: log.error(_("Failed to parse iTip events from message: %r" % (e))) itip_events = [] @@ -182,6 +203,13 @@ def execute(*args, **kw): auth.connect() 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 + if not len(resource_record_from_email_address(recipient)) == 0: resource_recipient = recipient any_resources = True @@ -209,6 +237,7 @@ def execute(*args, **kw): # check if resource attendees match the envelope recipient if len(resource_dns) == 0: log.info(_("No resource attendees matching envelope recipient %s, Reject message") % (resource_recipient)) + log.debug("%r" % (itip_events), level=8) reject(filepath) return False @@ -225,6 +254,58 @@ def execute(*args, **kw): receiving_resource = resources[resource_dns[0]] for itip_event in itip_events: + if itip_event['method'] == 'REPLY': + done = True + + # find initial reservation referenced by the reply + if reference_uid: + event = find_existing_event(reference_uid, receiving_resource) + if event: + try: + sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email) + owner_reply = sender_attendee.get_participant_status() + log.debug(_("Sender Attendee: %r => %r") % (sender_attendee, owner_reply), level=9) + except Exception, e: + log.error("Could not find envelope sender attendee: %r" % (e)) + continue + + # compare sequence number to avoid outdated replies + if not itip_event['sequence'] == event.get_sequence(): + log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Ignoring.") % ( + itip_event['sequence'], event.get_sequence() + )) + continue + + # forward owner response comment + comment = itip_event['xml'].get_comment() + if comment: + event.set_comment(str(comment)) + + itip_event_ = dict(xml=event, uid=event.get_uid()) + + if owner_reply == kolabformat.PartAccepted: + event.set_status(kolabformat.StatusConfirmed) + accept_reservation_request(itip_event_, receiving_resource, confirmed=True) + elif owner_reply == kolabformat.PartDeclined: + decline_reservation_request(itip_event_, receiving_resource) + # TODO: set status=CANCELLED instead of deleting? + # event.set_status(kolabformat.StatusCancelled) + delete_resource_event(reference_uid, receiving_resource) + else: + log.info("Invalid response (%r) recieved from resource owner for event %r" % ( + sender_attendee.get_participant_status(True), reference_uid + )) + else: + log.info(_("Event referenced by this REPLY (%r) not found in resource calendar") % (reference_uid)) + + else: + log.info(_("No event reference found in this REPLY. Ignoring.")) + + # exit for-loop + break + + # else: + try: receiving_attendee = itip_event['xml'].get_attendee_by_email(receiving_resource['mail']) log.debug(_("Receiving Resource: %r; %r") % (receiving_resource, receiving_attendee), level=9) @@ -466,97 +547,107 @@ def read_resource_calendar(resource_rec, itip_events): event_message = message_from_string(data[0][1]) - if event_message.is_multipart(): - for part in event_message.walk(): - if part.get_content_type() == "application/calendar+xml": - payload = part.get_payload(decode=True) - event = pykolab.xml.event_from_string(payload) + try: + event = event_from_message(message_from_string(data[0][1])) + except Exception, e: + log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e)) + continue + + if event: + for itip in itip_events: + conflict = check_event_conflict(event, itip) - for itip in itip_events: - _es = to_dt(event.get_start()) - _ee = to_dt(event.get_end()) + if event.get_uid() == itip['uid']: + resource_rec['existing_events'].append(itip['uid']) - conflict = False + if conflict: + log.info( + _("Event %r conflicts with event %r") % ( + itip['xml'].get_uid(), + event.get_uid() + ) + ) - # naive loops to check for collisions in (recurring) events - # TODO: compare recurrence rules directly (e.g. matching time slot or weekday or monthday) - while not conflict and _es is not None: - _is = to_dt(itip['start']) - _ie = to_dt(itip['end']) + resource_rec['conflicting_events'].append(event.get_uid()) + resource_rec['conflict'] = True - while not conflict and _is is not None: - log.debug("* Comparing event dates at %s/%s with %s/%s" % (_es, _ee, _is, _ie), level=9) - conflict = check_date_conflict(_es, _ee, _is, _ie) - _is = to_dt(itip['xml'].get_next_occurence(_is)) if event.is_recurring() else None - _ie = to_dt(itip['xml'].get_occurence_end_date(_is)) + return num_messages - _es = to_dt(event.get_next_occurence(_es)) if event.is_recurring() else None - _ee = to_dt(event.get_occurence_end_date(_es)) - if event.get_uid() == itip['uid']: - resource_rec['existing_events'].append(itip['uid']) +def find_existing_event(uid, resource_rec): + """ + Search the resources's calendar folder for the given event (by UID) + """ + global imap - # don't register conflict for updates - if itip['sequence'] > 0 and itip['sequence'] >= event.get_sequence(): - conflict = False + event = None + mailbox = resource_rec['kolabtargetfolder'] - if conflict: - log.info( - _("Event %r conflicts with event %r") % ( - itip['xml'].get_uid(), - event.get_uid() - ) - ) + log.debug(_("Searching %r for event %r") % (mailbox, uid), level=9) - resource_rec['conflicting_events'].append(event.get_uid()) - resource_rec['conflict'] = True + try: + imap.imap.m.select(imap.folder_quote(mailbox)) + typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) + except Exception, e: + log.error(_("Failed to access resource calendar:: %r") % (e)) + return event - return num_messages + for num in reversed(data[0].split()): + typ, data = imap.imap.m.fetch(num, '(RFC822)') -def check_date_conflict(_es, _ee, _is, _ie): - conflict = False + try: + event = event_from_message(message_from_string(data[0][1])) + except Exception, e: + log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e)) + continue - # TODO: add margin for all-day dates (+13h; -12h) + if event and event.uid == uid: + return event - if _es < _is: - if _es <= _ie: - if _ee <= _is: - conflict = False - else: - conflict = True - else: - conflict = True - elif _es == _is: - conflict = True - else: # _es > _is - if _es <= _ie: - conflict = True - else: - conflict = False - - return conflict + return event -def accept_reservation_request(itip_event, resource, delegator=None): +def accept_reservation_request(itip_event, resource, delegator=None, confirmed=False): """ Accepts the given iTip event by booking it into the resource's calendar. Then set the attendee status of the given resource to ACCEPTED and sends an iTip reply message to the organizer. """ + owner = get_resource_owner(resource) + confirmation_required = False + + if not confirmed: + invitationpolicy = get_resource_invitationpolicy(resource) + log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=9) + if invitationpolicy is not None: + for policy in invitationpolicy: + if policy & ACT_MANUAL and owner['mail']: + confirmation_required = True + break + + partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED' + + itip_event['xml'].set_transparency(False); itip_event['xml'].set_attendee_participant_status( itip_event['xml'].get_attendee_by_email(resource['mail']), - "ACCEPTED" + partstat ) - saved = save_resource_event(itip_event, resource) + saved = save_resource_event(itip_event, resource, replace=confirmed) log.debug( _("Adding event to %r: %r") % (resource['kolabtargetfolder'], saved), level=8 ) - send_response(delegator['mail'] if delegator else resource['mail'], itip_event, get_resource_owner(resource)) + if saved: + send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner) + + if owner and confirmation_required: + send_owner_confirmation(resource, owner, itip_event) + elif owner: + send_owner_notification(resource, owner, itip_event, saved) def decline_reservation_request(itip_event, resource): @@ -570,17 +661,27 @@ def decline_reservation_request(itip_event, resource): "DECLINED" ) + owner = get_resource_owner(resource) send_response(resource['mail'], itip_event, get_resource_owner(resource)) + if owner: + send_owner_notification(resource, owner, itip_event, True) + -def save_resource_event(itip_event, resource): +def save_resource_event(itip_event, resource, replace=False): """ Append the given event object to the resource's calendar """ try: # Administrator login name comes from configuration. targetfolder = imap.folder_quote(resource['kolabtargetfolder']) - imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda") + + # remove old copy of the reservation (also sets ACLs) + if replace: + delete_resource_event(itip_event['uid'], resource) + else: + imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda") + result = imap.imap.m.append( targetfolder, None, @@ -617,118 +718,6 @@ def delete_resource_event(uid, resource): imap.imap.m.expunge() -def itip_events_from_message(message): - """ - Obtain the iTip payload from email.message <message> - """ - # Placeholder for any itip_events found in the message. - itip_events = [] - seen_uids = [] - - # iTip methods we are actually interested in. Other methods will be ignored. - itip_methods = [ "REQUEST", "CANCEL" ] - - # Are all iTip messages multipart? No! RFC 6047, section 2.4 states "A - # MIME body part containing content information that conforms to this - # document MUST have (...)" but does not state whether an iTip message must - # therefore also be multipart. - - # Check each part - for part in message.walk(): - - # The iTip part MUST be Content-Type: text/calendar (RFC 6047, section 2.4) - # But in real word, other mime-types are used as well - if part.get_content_type() in [ "text/calendar", "text/x-vcalendar", "application/ics" ]: - if not str(part.get_param('method')).upper() in itip_methods: - log.error(_("Method %r not really interesting for us.") % (part.get_param('method'))) - continue - - # Get the itip_payload - itip_payload = part.get_payload(decode=True) - - log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9) - - # Python iCalendar prior to 3.0 uses "from_string". - if hasattr(icalendar.Calendar, 'from_ical'): - cal = icalendar.Calendar.from_ical(itip_payload) - elif hasattr(icalendar.Calendar, 'from_string'): - cal = icalendar.Calendar.from_string(itip_payload) - - # If we can't read it, we're out - else: - log.error(_("Could not read iTip from message.")) - return [] - - for c in cal.walk(): - if c.name == "VEVENT": - itip = {} - - if c['uid'] in seen_uids: - log.debug(_("Duplicate iTip event: %s") % (c['uid']), level=9) - continue - - # From the event, take the following properties: - # - # - method - # - uid - # - sequence - # - start - # - end (if any) - # - duration (if any) - # - organizer - # - attendees (if any) - # - resources (if any) - # - - itip['uid'] = str(c['uid']) - itip['method'] = str(cal['method']).upper() - itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0 - - if c.has_key('dtstart'): - itip['start'] = c['dtstart'].dt - else: - log.error(_("iTip event without a start")) - continue - - if c.has_key('dtend'): - itip['end'] = c['dtend'].dt - - if c.has_key('duration'): - itip['duration'] = c['duration'].dt - itip['end'] = itip['start'] + c['duration'].dt - - itip['organizer'] = c['organizer'] - - itip['attendees'] = c['attendee'] - - if c.has_key('resources'): - itip['resources'] = c['resources'] - - itip['raw'] = itip_payload - - try: - itip['xml'] = event_from_ical(c.to_ical()) - except Exception, e: - log.error("event_from_ical() exception: %r" % (e)) - continue - - itip_events.append(itip) - - seen_uids.append(c['uid']) - - # end if c.name == "VEVENT" - - # end for c in cal.walk() - - # end if part.get_content_type() == "text/calendar" - - # end for part in message.walk() - - if not len(itip_events) and not message.is_multipart(): - log.debug(_("Message is not an iTip message (non-multipart message)"), level=5) - - return itip_events - def reject(filepath): new_filepath = os.path.join( mybasepath, @@ -814,6 +803,12 @@ def resource_records_from_itip_events(itip_events, recipient_email=None): log.debug(_("Raw set of resources: %r") % (resources_raw), level=9) + # consider organizer (in REPLY messages), too + organizers_raw = [re.sub('\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')] + + log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8) + + # TODO: We expect the format of an attendee line to literally be: # # ATTENDEE:RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers@kolabsys.com @@ -822,7 +817,7 @@ def resource_records_from_itip_events(itip_events, recipient_email=None): # # RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers@kolabsys.com # - attendees = [x.split(':')[-1] for x in attendees_raw] + attendees = [x.split(':')[-1] for x in attendees_raw + organizers_raw] # Limit the attendee resources to the one that is actually invited # with the current message. Considering all invited resources would result in @@ -904,21 +899,25 @@ def get_resource_records(resource_dns): # If it is not, ... resource_attrs = auth.get_entry_attributes(None, resource_dn, ['*']) resource_attrs['dn'] = resource_dn + parse_kolabinvitationpolicy(resource_attrs) + if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]: if resource_attrs.has_key('uniquemember'): resources[resource_dn] = resource_attrs for uniquemember in resource_attrs['uniquemember']: - resource_attrs = auth.get_entry_attributes( + member_attrs = auth.get_entry_attributes( None, uniquemember, ['*'] ) - if 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]: - resource_attrs['dn'] = uniquemember - resources[uniquemember] = resource_attrs + if 'kolabsharedfolder' in [x.lower() for x in member_attrs['objectclass']]: + member_attrs['dn'] = uniquemember + parse_kolabinvitationpolicy(member_attrs, resource_attrs) + + resources[uniquemember] = member_attrs resources[uniquemember]['memberof'] = resource_dn - if not resource_attrs.has_key('owner') and resources[resource_dn].has_key('owner'): + if not member_attrs.has_key('owner') and resources[resource_dn].has_key('owner'): resources[uniquemember]['owner'] = resources[resource_dn]['owner'] resource_dns.append(uniquemember) else: @@ -927,6 +926,16 @@ def get_resource_records(resource_dns): return resources +def parse_kolabinvitationpolicy(attrs, parent=None): + if attrs.has_key('kolabinvitationpolicy'): + if not isinstance(attrs['kolabinvitationpolicy'], list): + attrs['kolabinvitationpolicy'] = [attrs['kolabinvitationpolicy']] + attrs['kolabinvitationpolicy'] = [policy_name_map[p] for p in attrs['kolabinvitationpolicy'] if policy_name_map.has_key(p)] + + elif isinstance(parent, dict) and parent.has_key('kolabinvitationpolicy'): + attrs['kolabinvitationpolicy'] = parent['kolabinvitationpolicy'] + + def get_resource_collection(email_address): """ @@ -979,6 +988,37 @@ def get_resource_owner(resource): return None +def get_resource_invitationpolicy(resource): + """ + Get this resource's kolabinvitationpolicy configuration + """ + global auth + + if not resource.has_key('kolabinvitationpolicy') or resource['kolabinvitationpolicy'] is None: + if not auth: + auth = Auth() + auth.connect() + + # get kolabinvitationpolicy attribute from collection + collections = auth.search_entry_by_attribute('uniquemember', resource['dn']) + if not isinstance(collections, list): + collections = [ (collections['dn'],collections) ] + + log.debug("Check collections %r for kolabinvitationpolicy attributes" % (collections), level=9) + + for dn,collection in collections: + # ldap.search_entry_by_attribute() doesn't return the attributes lower-cased + if collection.has_key('kolabInvitationPolicy'): + collection['kolabinvitationpolicy'] = collection['kolabInvitationPolicy'] + + if collection.has_key('kolabinvitationpolicy'): + parse_kolabinvitationpolicy(collection) + resource['kolabinvitationpolicy'] = collection['kolabinvitationpolicy'] + break + + return resource['kolabinvitationpolicy'] if resource.has_key('kolabinvitationpolicy') else None + + def send_response(from_address, itip_events, owner=None): """ Send the given iCal events as a valid iTip response to the organizer. @@ -986,12 +1026,6 @@ def send_response(from_address, itip_events, owner=None): resource, this will send an additional DELEGATED response message. """ - import smtplib - smtp = smtplib.SMTP("localhost", 10027) - - if conf.debuglevel > 8: - smtp.set_debuglevel(True) - if isinstance(itip_events, dict): itip_events = [ itip_events ] @@ -999,7 +1033,10 @@ def send_response(from_address, itip_events, owner=None): attendee = itip_event['xml'].get_attendee_by_email(from_address) participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee) + # TODO: look-up event organizer in LDAP and change localization to its preferredlanguage + message_text = reservation_response_text(participant_status, owner) + subject_template = _("Reservation Request for %(summary)s was %(status)s") if participant_status == "DELEGATED": # Extra actions to take @@ -1007,32 +1044,21 @@ def send_response(from_address, itip_events, owner=None): delegatee = [a for a in itip_event['xml'].get_attendees() if from_address in [b.email() for b in a.get_delegated_from()]][0] delegatee_status = itip_event['xml'].get_ical_attendee_participant_status(delegatee) - message = itip_event['xml'].to_message_itip(delegatee.get_email(), - method="REPLY", - participant_status=delegatee_status, - message_text=reservation_response_text(delegatee_status, owner) - ) - smtp.sendmail(message['From'], message['To'], message.as_string()) + pykolab.itip.send_reply(delegatee.get_email(), itip_event, reservation_response_text(delegatee_status, owner), + subject=subject_template) # restore list of attendees after to_message_itip() itip_event['xml']._attendees = [ delegator, delegatee ] itip_event['xml'].event.setAttendees(itip_event['xml']._attendees) - participant_status = "DELEGATED" message_text = _(""" *** This is an automated response, please do not reply! *** Your reservation was delegated to "%s" which is available for the requested time. """) % (delegatee.get_name()) - message = itip_event['xml'].to_message_itip(from_address, - method="REPLY", - participant_status=participant_status, - message_text=message_text - ) - smtp.sendmail(message['From'], message['To'], message.as_string()) - - smtp.quit() + pykolab.itip.send_reply(from_address, itip_event, message_text, + subject=subject_template) def reservation_response_text(status, owner): @@ -1040,7 +1066,7 @@ def reservation_response_text(status, owner): *** This is an automated response, please do not reply! *** We hereby inform you that your reservation was %s. - """) % (_(status)) + """) % (participant_status_label(status)) if owner: message_text += _(""" @@ -1049,3 +1075,145 @@ def reservation_response_text(status, owner): """) % (owner['cn'], owner['mail'], owner['telephoneNumber'] if owner.has_key('telephoneNumber') else '') return message_text + + +def send_owner_notification(resource, owner, itip_event, success=True): + """ + Send a reservation notification to the resource owner + """ + import smtplib + from pykolab import utils + from email.MIMEText import MIMEText + from email.Utils import formatdate + + notify = False + status = itip_event['xml'].get_attendee_by_email(resource['mail']).get_participant_status(True) + + invitationpolicy = get_resource_invitationpolicy(resource) + + if invitationpolicy is not None: + for policy in invitationpolicy: + # TODO: distingish ACCEPTED / DECLINED status notifications? + if policy & COND_NOTIFY and owner['mail']: + notify = True + break + + if notify or not success: + log.debug( + _("Sending booking notification for event %r to %r from %r") % ( + itip_event['uid'], owner['mail'], resource['cn'] + ), + level=8 + ) + + # change gettext language to the preferredlanguage setting of the resource owner + if owner.has_key('preferredlanguage'): + pykolab.translate.setUserLanguage(owner['preferredlanguage']) + + message_text = owner_notification_text(resource, owner, itip_event['xml'], success) + + msg = MIMEText(utils.stripped_message(message_text)) + + msg['To'] = owner['mail'] + msg['From'] = resource['mail'] + msg['Date'] = formatdate(localtime=True) + msg['Subject'] = _('Booking for %s has been %s') % (resource['cn'], participant_status_label(status) if success else _('failed')) + + smtp = smtplib.SMTP("localhost", 10027) + + if conf.debuglevel > 8: + smtp.set_debuglevel(True) + + try: + smtp.sendmail(resource['mail'], owner['mail'], msg.as_string()) + except Exception, e: + log.error(_("SMTP sendmail error: %r") % (e)) + + smtp.quit() + +def owner_notification_text(resource, owner, event, success): + organizer = event.get_organizer() + status = event.get_attendee_by_email(resource['mail']).get_participant_status(True) + + if success: + message_text = _(""" + The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s. + + *** This is an automated message, sent to you as the resource owner. *** + """) + else: + message_text = _(""" + A reservation request for %(resource)s could not be processed automatically. + Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s. + + *** This is an automated message, sent to you as the resource owner. *** + """) + + return message_text % { + 'resource': resource['cn'], + 'summary': event.get_summary(), + 'date': event.get_date_text(), + 'status': participant_status_label(status), + 'orgname': organizer.name(), + 'orgemail': organizer.email() + } + + +def send_owner_confirmation(resource, owner, itip_event): + """ + Send a reservation request to the resource owner for manual confirmation (ACCEPT or DECLINE) + + This clones the given invtation with a new UID and setting the resource as organizer in order to + receive the reply from the owner. + """ + + uid = itip_event['uid'] + event = itip_event['xml'] + organizer = event.get_organizer() + event_attendees = [a.get_displayname() for a in event.get_attendees() if not a.get_cutype() == kolabformat.CutypeResource] + + # generate new UID and set the resource as organizer + (mail, domain) = resource['mail'].split('@') + event.set_uid(str(uuid.uuid4())) + event.set_organizer(mail + '+' + base64.b64encode(uid, '-/') + '@' + domain, resource['cn']) + itip_event['uid'] = event.get_uid() + + # add resource owner as (the sole) attendee + event._attendees = [] + event.add_attendee(owner['mail'], owner['cn'], rsvp=True, role=kolabformat.Required, participant_status=kolabformat.PartNeedsAction) + + # flag this iTip message as confirmation type + event.add_custom_property('X-Kolab-InvitationType', 'CONFIRMATION') + + log.debug( + _("Clone invitation for owner confirmation: %r from %r") % ( + itip_event['uid'], event.get_organizer().email() + ), + level=8 + ) + + message_text = _(""" + A reservation request for %(resource)s requires your approval! + Please either accept or decline this inivitation without saving it to your calendar. + + The reservation request was sent from %(orgname)s <%(orgemail)s>. + + Subject: %(summary)s. + Date: %(date)s + Participants: %(attendees)s + + *** This is an automated message, please don't reply by email. *** + """)% { + 'resource': resource['cn'], + 'orgname': organizer.name(), + 'orgemail': organizer.email(), + 'summary': event.get_summary(), + 'date': event.get_date_text(), + 'attendees': ",\n+ ".join(event_attendees) + } + + pykolab.itip.send_request(owner['mail'], itip_event, message_text, + subject=_('Booking request for %s requires confirmation') % (resource['cn']), + direct=True) + + |