aboutsummaryrefslogtreecommitdiffstats
path: root/calendar
diff options
context:
space:
mode:
authorDaniel Lange <DLange@git.local>2016-03-07 15:53:16 +0100
committerDaniel Lange <DLange@git.local>2016-03-07 15:53:16 +0100
commit50569114acdc64e7c7cae1498635d3f821517c30 (patch)
tree13d6fe76af33134fbfb2286930fb6603047f9299 /calendar
parentc210d30de6c62e7f7867bb32651349ddf455d9e6 (diff)
downloadroundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.tar.gz
roundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.tar.bz2
roundcube_calendar-50569114acdc64e7c7cae1498635d3f821517c30.zip
Initial commit of the Faster IT roundcube_calendar plugin distribution
This includes: * Kolab plugins 3.2.9 (calendar and libcalendaring) * CalDAV driver 3.2.8 * .htaccess files for at least some security * SabreDAV updated to 1.8.12 (Jan 2015 release) * Support for CURLOPT_SSL_* settings to allow self-signed certificates * Small fixes & improved documentation
Diffstat (limited to 'calendar')
-rw-r--r--calendar/.gitignore8
-rw-r--r--calendar/README_kolab78
-rw-r--r--calendar/TODO48
-rw-r--r--calendar/UPGRADING17
-rw-r--r--calendar/calendar.php3581
-rw-r--r--calendar/calendar_base.js139
-rw-r--r--calendar/calendar_ui.js4273
-rw-r--r--calendar/composer.json31
-rw-r--r--calendar/config.inc.php.dist198
-rw-r--r--calendar/drivers/caldav/SQL/mysql.initial.sql92
-rw-r--r--calendar/drivers/caldav/SQL/mysql/.keep_dir0
-rw-r--r--calendar/drivers/caldav/SQL/mysql/2014081300.sql24
-rw-r--r--calendar/drivers/caldav/SQL/mysql/2015022500.sql125
-rw-r--r--calendar/drivers/caldav/SQL/mysql/2015022700.sql14
-rw-r--r--calendar/drivers/caldav/SQL/postgres.initial.sql51
-rw-r--r--calendar/drivers/caldav/caldav_driver.php2036
-rw-r--r--calendar/drivers/caldav/caldav_sync.php253
-rw-r--r--calendar/drivers/calendar_driver.php819
-rw-r--r--calendar/drivers/database/SQL/mysql.initial.sql85
-rw-r--r--calendar/drivers/database/SQL/mysql/2012080600.sql3
-rw-r--r--calendar/drivers/database/SQL/mysql/2013011000.sql1
-rw-r--r--calendar/drivers/database/SQL/mysql/2013042700.sql1
-rw-r--r--calendar/drivers/database/SQL/mysql/2013051600.sql3
-rw-r--r--calendar/drivers/database/SQL/mysql/2014040900.sql3
-rw-r--r--calendar/drivers/database/SQL/mysql/2015022700.sql15
-rw-r--r--calendar/drivers/database/SQL/postgres.initial.sql109
-rw-r--r--calendar/drivers/database/SQL/postgres/2012080600.sql3
-rw-r--r--calendar/drivers/database/SQL/postgres/2013011000.sql1
-rw-r--r--calendar/drivers/database/SQL/postgres/2013042700.sql8
-rw-r--r--calendar/drivers/database/SQL/postgres/2013051600.sql3
-rw-r--r--calendar/drivers/database/SQL/postgres/2014040900.sql3
-rw-r--r--calendar/drivers/database/SQL/postgres/2015022700.sql9
-rw-r--r--calendar/drivers/database/SQL/sqlite.initial.sql79
-rw-r--r--calendar/drivers/database/SQL/sqlite/2013011000.sql1
-rw-r--r--calendar/drivers/database/SQL/sqlite/2013042700.sql1
-rw-r--r--calendar/drivers/database/SQL/sqlite/2013051600.sql63
-rw-r--r--calendar/drivers/database/SQL/sqlite/2014040900.sql67
-rw-r--r--calendar/drivers/database/SQL/sqlite/2015022700.sql79
-rw-r--r--calendar/drivers/database/database_driver.php1496
-rw-r--r--calendar/drivers/ical/SQL/mysql.initial.sql91
-rw-r--r--calendar/drivers/ical/SQL/mysql/.keep_dir0
-rw-r--r--calendar/drivers/ical/SQL/mysql/2015022500.sql124
-rw-r--r--calendar/drivers/ical/SQL/mysql/2015022700.sql14
-rw-r--r--calendar/drivers/ical/ical_driver.php1821
-rw-r--r--calendar/drivers/ical/ical_sync.php125
-rw-r--r--calendar/drivers/kolab/SQL/mysql.initial.sql32
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2012080600.sql11
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2013011000.sql1
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2014041700.sql1
-rw-r--r--calendar/drivers/kolab/SQL/mysql/2014082600.sql2
-rw-r--r--calendar/drivers/kolab/SQL/oracle.initial.sql31
-rw-r--r--calendar/drivers/kolab/SQL/postgres.initial.sql32
-rw-r--r--calendar/drivers/kolab/kolab_calendar.php836
-rw-r--r--calendar/drivers/kolab/kolab_driver.php2526
-rw-r--r--calendar/drivers/kolab/kolab_invitation_calendar.php377
-rw-r--r--calendar/drivers/kolab/kolab_user_calendar.php432
-rw-r--r--calendar/drivers/ldap/resources_driver_ldap.php150
-rw-r--r--calendar/drivers/resources_driver.php114
l---------calendar/helpdocs/en_US/_static/_skin1
l---------calendar/helpdocs/en_US/_static/kolab/alarms-popup.png1
-rw-r--r--calendar/helpdocs/en_US/_static/kolab/calendar-acl.pngbin0 -> 108292 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/kolab/calendar-header.pngbin0 -> 13036 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/kolab/event-participants.pngbin0 -> 101779 bytes
l---------calendar/helpdocs/en_US/_static/kolab/event-resize.png1
l---------calendar/helpdocs/en_US/_static/kolab/itip-invitation.png1
l---------calendar/helpdocs/en_US/_static/kolab/itip-reply.png1
-rw-r--r--calendar/helpdocs/en_US/_static/larry/alarms-popup.pngbin0 -> 19181 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/calendar-acl.pngbin0 -> 28590 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/calendar-header.pngbin0 -> 23126 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/event-participants.pngbin0 -> 39276 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/event-resize.pngbin0 -> 23728 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/itip-invitation.pngbin0 -> 117940 bytes
-rw-r--r--calendar/helpdocs/en_US/_static/larry/itip-reply.pngbin0 -> 39276 bytes
-rw-r--r--calendar/helpdocs/en_US/importexport.rst43
-rw-r--r--calendar/helpdocs/en_US/index.rst18
-rw-r--r--calendar/helpdocs/en_US/invitations.rst55
-rw-r--r--calendar/helpdocs/en_US/manage.rst169
-rw-r--r--calendar/helpdocs/en_US/overview.rst138
-rw-r--r--calendar/helpdocs/en_US/settings.rst75
-rw-r--r--calendar/helpdocs/en_US/sharing.rst49
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/da_DK/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/de_CH/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/importexport.po97
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/index.po29
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/invitations.po104
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/manage.po343
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/overview.po267
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/settings.po179
-rw-r--r--calendar/helpdocs/locale/de_DE/LC_MESSAGES/sharing.po100
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/en_US/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/es_AR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/es_ES/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/et_EE/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/it_IT/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/importexport.po96
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/index.po28
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/invitations.po103
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/manage.po342
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/overview.po266
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/settings.po178
-rw-r--r--calendar/helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po99
-rw-r--r--calendar/helpdocs/po/importexport.pot86
-rw-r--r--calendar/helpdocs/po/index.pot26
-rw-r--r--calendar/helpdocs/po/invitations.pot74
-rw-r--r--calendar/helpdocs/po/manage.pot250
-rw-r--r--calendar/helpdocs/po/overview.pot195
-rw-r--r--calendar/helpdocs/po/settings.pot146
-rw-r--r--calendar/helpdocs/po/sharing.pot74
-rw-r--r--calendar/lib/SabreDAV/.htaccess2
-rw-r--r--calendar/lib/SabreDAV/ChangeLog1111
-rw-r--r--calendar/lib/SabreDAV/LICENSE27
-rw-r--r--calendar/lib/SabreDAV/README.md30
-rw-r--r--calendar/lib/SabreDAV/composer.json61
-rw-r--r--calendar/lib/SabreDAV/composer.lock80
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php155
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php233
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php47
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php691
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php243
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php376
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php279
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php298
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php392
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php142
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php48
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php173
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php38
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php44
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php192
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php324
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php218
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php182
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php1338
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php32
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php180
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php180
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php134
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php74
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php227
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php102
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php88
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php40
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php45
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php16
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php163
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php72
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php116
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php526
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php342
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php315
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php221
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php80
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php18
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php166
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php333
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php260
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php20
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php20
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php706
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php72
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php260
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php108
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php26
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php87
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php101
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php63
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php36
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php65
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php112
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php99
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php57
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php491
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.icobin0 -> 4286 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.pngbin0 -> 7232 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.pngbin0 -> 4388 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.pngbin0 -> 5695 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.pngbin0 -> 3474 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.pngbin0 -> 2837 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.pngbin0 -> 3474 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.pngbin0 -> 5480 bytes
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php578
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php110
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php64
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php37
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php19
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php33
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php41
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php73
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php45
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php71
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php32
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php31
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php30
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php140
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php91
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php82
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php159
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php146
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php214
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php85
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php28
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php46
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php71
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php27
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php51
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php193
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php183
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php167
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php81
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php649
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php83
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php55
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php159
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php39
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php48
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php246
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php31
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php78
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php99
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php105
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php25
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php104
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php127
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php157
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php59
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php78
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php21
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php2178
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php90
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php108
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php121
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php91
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php289
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php193
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php133
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php124
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php64
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php191
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php155
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php83
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php35
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php74
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php77
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php42
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php1402
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php281
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php18
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php153
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php428
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php33
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php211
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php34
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php124
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php161
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php94
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php227
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php111
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php67
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php240
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php284
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php175
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php82
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php24
-rw-r--r--calendar/lib/SabreDAV/lib/OldSabre/autoload.php25
-rw-r--r--calendar/lib/SabreDAV/vendor/autoload.php7
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/ClassLoader.php387
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php9
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php15
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php9
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/autoload_real.php50
-rw-r--r--calendar/lib/SabreDAV/vendor/composer/installed.json52
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog70
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE27
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md384
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json31
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php405
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php108
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php244
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php107
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php70
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php68
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php46
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php68
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php181
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php109
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php172
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php322
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php187
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php100
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php12
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php442
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php125
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php245
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php180
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php223
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php1112
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php111
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php39
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php76
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php61
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php482
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php24
-rw-r--r--calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php41
-rw-r--r--calendar/lib/caldav-client.php379
-rw-r--r--calendar/lib/calendar_itip.php240
-rw-r--r--calendar/lib/calendar_recurrence.php88
-rw-r--r--calendar/lib/calendar_ui.php918
-rw-r--r--calendar/lib/encryption.php166
-rw-r--r--calendar/lib/js/fullcalendar.js6845
-rw-r--r--calendar/localization/bg_BG.inc122
-rw-r--r--calendar/localization/ca_ES.inc266
-rw-r--r--calendar/localization/cs_CZ.inc273
-rw-r--r--calendar/localization/da_DK.inc274
-rw-r--r--calendar/localization/de_CH.inc184
-rw-r--r--calendar/localization/de_DE.inc284
-rw-r--r--calendar/localization/en_US.inc315
-rw-r--r--calendar/localization/es_AR.inc269
-rw-r--r--calendar/localization/es_ES.inc26
-rw-r--r--calendar/localization/fi_FI.inc274
-rw-r--r--calendar/localization/fr_FR.inc271
-rw-r--r--calendar/localization/hu_HU.inc216
-rw-r--r--calendar/localization/it_IT.inc273
-rw-r--r--calendar/localization/ja_JP.inc177
-rw-r--r--calendar/localization/nl_NL.inc229
-rw-r--r--calendar/localization/pl_PL.inc272
-rw-r--r--calendar/localization/pt_BR.inc213
-rw-r--r--calendar/localization/pt_PT.inc274
-rw-r--r--calendar/localization/ru_RU.inc277
-rw-r--r--calendar/localization/sk_SK.inc82
-rw-r--r--calendar/localization/sl_SI.inc273
-rw-r--r--calendar/localization/sv_SE.inc273
-rw-r--r--calendar/localization/th_TH.inc257
-rw-r--r--calendar/localization/uk_UA.inc228
-rw-r--r--calendar/localization/zh_CN.inc76
-rw-r--r--calendar/print.js176
-rw-r--r--calendar/skins/larry/README11
-rw-r--r--calendar/skins/larry/calendar.css2327
-rw-r--r--calendar/skins/larry/fullcalendar.css711
-rw-r--r--calendar/skins/larry/iehacks.css77
-rw-r--r--calendar/skins/larry/images/attendee-status.pngbin0 -> 2202 bytes
-rw-r--r--calendar/skins/larry/images/autocomplete.pngbin0 -> 558 bytes
-rw-r--r--calendar/skins/larry/images/badge_cancelled.pngbin0 -> 924 bytes
-rw-r--r--calendar/skins/larry/images/badge_confidential.pngbin0 -> 1522 bytes
-rw-r--r--calendar/skins/larry/images/badge_private.pngbin0 -> 1346 bytes
-rw-r--r--calendar/skins/larry/images/calendar.pngbin0 -> 613 bytes
-rw-r--r--calendar/skins/larry/images/calendars.pngbin0 -> 2582 bytes
-rw-r--r--calendar/skins/larry/images/eventicons.pngbin0 -> 217 bytes
-rw-r--r--calendar/skins/larry/images/focusview.pngbin0 -> 4224 bytes
-rw-r--r--calendar/skins/larry/images/freebusy-colors.pngbin0 -> 302 bytes
-rw-r--r--calendar/skins/larry/images/ical-attachment.pngbin0 -> 492 bytes
-rw-r--r--calendar/skins/larry/images/invitation.pngbin0 -> 1485 bytes
-rw-r--r--calendar/skins/larry/images/loading_blue.gifbin0 -> 847 bytes
-rw-r--r--calendar/skins/larry/images/sendinvitation.pngbin0 -> 337 bytes
-rw-r--r--calendar/skins/larry/images/toggle.gifbin0 -> 110 bytes
-rw-r--r--calendar/skins/larry/images/toolbar.pngbin0 -> 3662 bytes
-rw-r--r--calendar/skins/larry/print.css229
-rw-r--r--calendar/skins/larry/print.iehacks.css25
-rw-r--r--calendar/skins/larry/templates/attachment.html64
-rw-r--r--calendar/skins/larry/templates/calendar.html537
-rw-r--r--calendar/skins/larry/templates/eventedit.html133
-rw-r--r--calendar/skins/larry/templates/freebusylegend.html7
-rw-r--r--calendar/skins/larry/templates/itipattend.html37
-rw-r--r--calendar/skins/larry/templates/kolabacl.html26
-rw-r--r--calendar/skins/larry/templates/kolabform.html9
-rw-r--r--calendar/skins/larry/templates/print.html29
599 files changed, 109959 insertions, 0 deletions
diff --git a/calendar/.gitignore b/calendar/.gitignore
new file mode 100644
index 0000000..7c2f14c
--- /dev/null
+++ b/calendar/.gitignore
@@ -0,0 +1,8 @@
+*.swp
+*.bak
+*.old
+*~
+config.inc.php
+skins/*
+!skins/default
+!skins/larry \ No newline at end of file
diff --git a/calendar/README_kolab b/calendar/README_kolab
new file mode 100644
index 0000000..2426fcf
--- /dev/null
+++ b/calendar/README_kolab
@@ -0,0 +1,78 @@
+A calendar module for Roundcube
+-------------------------------
+
+This plugin currently supports a local database as well as a Kolab groupware
+server as backends for calendar and event storage. For both drivers, some
+initialization of the local database is necessary. To do so, execute the
+SQL commands in drivers/<yourchoice>/SQL/<yourdatabase>.initial.sql
+
+The client-side calendar UI relies on the "fullcalendar" project by Adam Arshaw
+with extensions made for the use in Roundcube. All changes are published in
+an official fork at https://github.com/roundcube/fullcalendar
+
+For some general calendar-based operations such as alarms handling or iCal
+parsing/exporting this plugins requires the `libcalendaring` plugin which
+is also part of the Kolab Roundcube Plugins repository. Make sure that plugin
+is installed and configured correctly.
+
+For recurring event computation, some utility classes from the Horde project
+are used. They are packaged in a slightly modified version with this plugin.
+
+IMPORTANT
+---------
+
+The calendar module makes heavy use of PHP's DateTime as well as DateInterval
+classes. The latter one requires at least PHP 5.3.0 to run.
+
+
+REQUIREMENTS
+------------
+
+Some functions are shared with other plugins and therefore being moved to
+library plugins. Thus in order to run the calendar plugin, you also need the
+following plugins installed:
+
+* libcalendaring [1]
+* libkolab [1] (when using the 'kolab' driver)
+
+
+INSTALLATION
+------------
+
+For a manual installation of the calendar plugin (and its dependencies),
+execute the following steps. This will set it up with the database backend
+driver.
+
+1. Get the source from git
+
+ $ cd /tmp
+ $ git clone git://git.kolab.org/git/roundcubemail-plugins-kolab
+ $ cd /<path-to-roundcube>/plugins
+ $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/calendar .
+ $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libcalendaring .
+
+2. Create calendar plugin configuration
+
+ $ cd calendar/
+ $ cp config.inc.php.dist config.inc.php
+ $ edit config.inc.php
+
+3. Initialize the calendar database tables
+
+ $ mysql roundcubemail < drivers/database/SQL/mysql.initial.sql
+
+4. Enable the calendar plugin
+
+ $ cd ../../
+ $ edit config/config.inc.php
+
+Add 'calendar' to the list of active plugins:
+
+ $config['plugins'] = array(
+ (...)
+ 'calendar',
+ );
+
+
+
+[1] http://git.kolab.org/roundcubemail-plugins-kolab/
diff --git a/calendar/TODO b/calendar/TODO
new file mode 100644
index 0000000..b1a08d7
--- /dev/null
+++ b/calendar/TODO
@@ -0,0 +1,48 @@
++ Edit: 3.12: Subject
++ Edit: 3.13: Location
++ Edit: 3.14: Start / End / All Day
++ Edit: 3.15: Show time as: Busy, Free, Out of office
++ Edit: 3.16: Reminder set
++ Edit: 3.17: Priority: High/Low
++ Edit: 3.18: Recurrence (in line with Kontact)
++ Edit: 3.19: Attachment Upload
++ Edit: 3.20: Print
++ Add/Manage Attendees
+ + Edit: 3.21: Required / Optional / Resource specification
+ + Edit: 3.22: Conflict Handling (Free/Busy Check for attendees)
++ View: 3.3: Display modes (agenda / day / week / month)
+ + Day / Week / Month
+ + List (Agenda) view
+ + Add selection for date range
+ - Individual days selection
++ Show list of calendars in a (hideable) drawer
+ + View: 3.1: Folder list
+ + View: 3.2: Add / Remove / Rename / Share Folders
+ + View: 3.6: Combined calendar view (Turn calendars on/off)
+ + View: 3.7: Small month overview calendar
++ View: 3.5: Search
+ - Filter by categories (similar to mail)
++ View: 3.9: Alter event with drag/drop
++ Option: 4.12: Set default reminder time
++ Option: 3.23: Specify folder for new event (prefs)
++ Option: Set date/time format in prefs
++ Receive: 1.20: Invitation handling
+ - Jump to calendar view from mail ("Show event")
+ - Allow to re-send invitations
+ - Implement iTIP delegation
+
++ View: 3.4: Fish-Eye View For Busy Days
++ View: 3.8: Color according to calendar and category (similar to Kontact)
+
++ Support for multiple calendars (replace categories)
++ Allow user to create/edit/delete calendars
++ Colors for calendars should be user-configurable
++ ICS parser/generator (http://code.google.com/p/qcal/)
+
+- Script to send event alarms by email (in cronjob)
+- Export *with* attachments
+- Remember last visited view
+- Create/manage invdividual views
++ Importing ICS files (upload, drag & drop)
+
+
diff --git a/calendar/UPGRADING b/calendar/UPGRADING
new file mode 100644
index 0000000..0e36e85
--- /dev/null
+++ b/calendar/UPGRADING
@@ -0,0 +1,17 @@
+UPGRADING instructions
+======================
+
+To update database schema, depending on the driver you're using,
+please run in Roundcube bin/ directory:
+
+updatedb.sh --package=calendar-<driver> --version=<version> \
+ --dir=../plugins/calendar/drivers/<driver>/SQL
+
+[*] Replace <driver> with "database" or "kolab" (without quotes)
+[*] Replace <version> with Roundcube version e.g. 0.9.0
+[*] Roundcube should be upgraded before plugin upgrades
+
+Example:
+
+updatedb.sh --package=calendar-kolab --version=0.9.0 \
+ --dir=../plugins/calendar/drivers/kolab/SQL
diff --git a/calendar/calendar.php b/calendar/calendar.php
new file mode 100644
index 0000000..cb32773
--- /dev/null
+++ b/calendar/calendar.php
@@ -0,0 +1,3581 @@
+<?php
+
+/**
+ * Calendar plugin for Roundcube webmail
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class calendar extends rcube_plugin
+{
+ const FREEBUSY_UNKNOWN = 0;
+ const FREEBUSY_FREE = 1;
+ const FREEBUSY_BUSY = 2;
+ const FREEBUSY_TENTATIVE = 3;
+ const FREEBUSY_OOF = 4;
+
+ const SESSION_KEY = 'calendar_temp';
+
+ public $task = '?(?!logout).*';
+ public $rc;
+ public $lib;
+ private $_drivers = null;
+ private $_cals = null;
+ private $_cal_driver_map = null;
+ public $resources_dir;
+ public $home; // declare public to be used in other classes
+ public $urlbase;
+ public $timezone;
+ public $timezone_offset;
+ public $gmt_offset;
+ public $ui;
+
+ public $defaults = array(
+ 'calendar_default_view' => "agendaWeek",
+ 'calendar_timeslots' => 2,
+ 'calendar_work_start' => 6,
+ 'calendar_work_end' => 18,
+ 'calendar_agenda_range' => 60,
+ 'calendar_agenda_sections' => 'smart',
+ 'calendar_event_coloring' => 0,
+ 'calendar_time_indicator' => true,
+ 'calendar_allow_invite_shared' => false,
+ 'calendar_itip_send_option' => 3,
+ 'calendar_itip_after_action' => 0,
+ );
+
+ private $ical;
+ private $itip;
+
+ /**
+ * Plugin initialization.
+ */
+ function init()
+ {
+ $this->require_plugin('libcalendaring');
+
+ $this->rc = rcube::get_instance();
+ $this->lib = libcalendaring::get_instance();
+
+ $this->register_task('calendar', 'calendar');
+
+ // load calendar configuration
+ $this->load_config();
+
+ // load localizations
+ $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
+
+ $this->timezone = $this->lib->timezone;
+ $this->gmt_offset = $this->lib->gmt_offset;
+ $this->dst_active = $this->lib->dst_active;
+ $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
+
+ require($this->home . '/lib/calendar_ui.php');
+ $this->ui = new calendar_ui($this);
+
+ // catch iTIP confirmation requests that don're require a valid session
+ if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
+ $this->add_hook('startup', array($this, 'itip_attend_response'));
+ }
+ else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
+ $this->add_hook('startup', array($this, 'ical_feed_export'));
+ }
+ else {
+ // default startup routine
+ $this->add_hook('startup', array($this, 'startup'));
+ }
+
+ $this->add_hook('user_delete', array($this, 'user_delete'));
+ }
+
+ /**
+ * Startup hook
+ */
+ public function startup($args)
+ {
+ // the calendar module can be enabled/disabled by the kolab_auth plugin
+ if ($this->rc->config->get('calendar_disabled', false) || !$this->rc->config->get('calendar_enabled', true))
+ return;
+
+ // load Calendar user interface
+ if (!$this->rc->output->ajax_call && (!$this->rc->output->env['framed'] || $args['action'] == 'preview')) {
+ $this->ui->init();
+
+ // settings are required in (almost) every GUI step
+ if ($args['action'] != 'attend')
+ $this->rc->output->set_env('calendar_settings', $this->load_settings());
+ }
+
+ if ($args['task'] == 'calendar' && $args['action'] != 'save-pref') {
+ // Load drivers to register possible hooks.
+ $this->load_drivers();
+
+ // register calendar actions
+ $this->register_action('index', array($this, 'calendar_view'));
+ $this->register_action('event', array($this, 'event_action'));
+ $this->register_action('calendar', array($this, 'calendar_action'));
+ $this->register_action('count', array($this, 'count_events'));
+ $this->register_action('load_events', array($this, 'load_events'));
+ $this->register_action('export_events', array($this, 'export_events'));
+ $this->register_action('import_events', array($this, 'import_events'));
+ $this->register_action('upload', array($this, 'attachment_upload'));
+ $this->register_action('get-attachment', array($this, 'attachment_get'));
+ $this->register_action('freebusy-status', array($this, 'freebusy_status'));
+ $this->register_action('freebusy-times', array($this, 'freebusy_times'));
+ $this->register_action('randomdata', array($this, 'generate_randomdata'));
+ $this->register_action('print', array($this,'print_view'));
+ $this->register_action('mailimportitip', array($this, 'mail_import_itip'));
+ $this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
+ $this->register_action('mailtoevent', array($this, 'mail_message2event'));
+ $this->register_action('inlineui', array($this, 'get_inline_ui'));
+ $this->register_action('check-recent', array($this, 'check_recent'));
+ $this->register_action('itip-status', array($this, 'event_itip_status'));
+ $this->register_action('itip-remove', array($this, 'event_itip_remove'));
+ $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
+ $this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
+ $this->register_action('resources-list', array($this, 'resources_list'));
+ $this->register_action('resources-owner', array($this, 'resources_owner'));
+ $this->register_action('resources-calendar', array($this, 'resources_calendar'));
+ $this->register_action('resources-autocomplete', array($this, 'resources_autocomplete'));
+ $this->add_hook('refresh', array($this, 'refresh'));
+
+ // remove undo information...
+ if ($undo = $_SESSION['calendar_event_undo']) {
+ // ...after timeout
+ $undo_time = $this->rc->config->get('undo_timeout', 0);
+ if ($undo['ts'] < time() - $undo_time) {
+ $this->rc->session->remove('calendar_event_undo');
+ // @TODO: do EXPUNGE on kolab objects?
+ }
+ }
+
+ // loading preinstalled calendars
+ $preinstalled_calendars = $this->rc->config->get('calendar_preinstalled_calendars', FALSE);
+ if ($preinstalled_calendars && is_array($preinstalled_calendars)) {
+
+ // expanding both caldav url and user with RC (imap) username
+ foreach ($preinstalled_calendars as $index => $cal){
+ $preinstalled_calendars[$index]['caldav_url'] = str_replace('%u', $this->rc->get_user_name(), $cal['caldav_url']);
+ $preinstalled_calendars[$index]['caldav_user'] = str_replace('%u', $this->rc->get_user_name(), $cal['caldav_user']);
+ }
+
+ foreach ($this->get_drivers() as $driver_name => $driver) {
+ foreach ($preinstalled_calendars as $cal) {
+ if ($driver_name == $cal['driver']) {
+ if (!$driver->insert_default_calendar($cal)) {
+ $error_msg = 'Unable to add default calendars' . ($driver && $driver->last_error ? ': ' . $driver->last_error :'');
+ $this->rc->output->show_message($error_msg, 'error');
+ }
+ }
+ }
+ }
+ }
+ }
+ else if ($args['task'] == 'settings') {
+ // add hooks for Calendar settings
+ $this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
+ $this->add_hook('preferences_list', array($this, 'preferences_list'));
+ $this->add_hook('preferences_save', array($this, 'preferences_save'));
+ }
+ else if ($args['task'] == 'mail') {
+ // hooks to catch event invitations on incoming mails
+ if ($args['action'] == 'show' || $args['action'] == 'preview') {
+ $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
+ }
+
+ // add 'Create event' item to message menu
+ if ($this->api->output->type == 'html') {
+ $this->api->add_content(html::tag('li', null,
+ $this->api->output->button(array(
+ 'command' => 'calendar-create-from-mail',
+ 'label' => 'calendar.createfrommail',
+ 'type' => 'link',
+ 'classact' => 'icon calendarlink active',
+ 'class' => 'icon calendarlink',
+ 'innerclass' => 'icon calendar',
+ ))),
+ 'messagemenu');
+
+ $this->api->output->add_label('calendar.createfrommail');
+ }
+
+ $this->add_hook('messages_list', array($this, 'mail_messages_list'));
+ $this->add_hook('message_compose', array($this, 'mail_message_compose'));
+ }
+ else if ($args['task'] == 'addressbook') {
+ if ($this->rc->config->get('calendar_contact_birthdays')) {
+ $this->add_hook('contact_update', array($this, 'contact_update'));
+ $this->add_hook('contact_create', array($this, 'contact_update'));
+ }
+ }
+
+ // add hooks to display alarms
+ $this->add_hook('pending_alarms', array($this, 'pending_alarms'));
+ $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
+ }
+
+ /**
+ * Helper method to load all configured drivers.
+ */
+ public function load_drivers()
+ {
+ if($this->_drivers == null)
+ {
+ $this->_drivers = array();
+
+ foreach($this->get_driver_names() as $driver_name)
+ {
+ $driver_name = trim($driver_name);
+ $driver_class = $driver_name . '_driver';
+
+ require_once($this->home . '/drivers/calendar_driver.php');
+ require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+
+ if($driver_name == "kolab")
+ $this->require_plugin('libkolab');
+
+ $driver = new $driver_class($this);
+
+ if ($driver->undelete)
+ $driver->undelete = $this->rc->config->get('undo_timeout', 0) > 0;
+
+ $this->_drivers[$driver_name] = $driver;
+ }
+ }
+ }
+
+ /*
+ * Helper method to get configured driver names.
+ * @return List of driver names.
+ */
+ public function get_driver_names()
+ {
+ $driver_names = $this->rc->config->get('calendar_driver', array('kolab'));
+ if(!is_array($driver_names)) $driver_names = array($driver_names);
+ return $driver_names;
+ }
+
+ /**
+ * Helpers function to return loaded drivers.
+ * @return List of driver objects.
+ */
+ public function get_drivers()
+ {
+ $this->load_drivers();
+ return $this->_drivers;
+ }
+
+ /**
+ * Helper method to get driver by name.
+ *
+ * @param string $name Driver name to get driver object for.
+ * @return mixed Driver object or null if no such driver exists.
+ */
+ public function get_driver_by_name($name)
+ {
+ $this->load_drivers();
+ if(isset($this->_drivers[$name]))
+ {
+ return $this->_drivers[$name];
+ }
+ else
+ {
+ rcube::raise_error("Unknown driver requested \"$name\".", true, true);
+ return null;
+ }
+ }
+
+ /**
+ * Helper method to get the driver by GPC input, e.g. "_driver" or "driver"
+ * property specified in POST/GET or COOKIE variables.
+ *
+ * @param boolean $quiet = false Indicates where to raise an error if no driver was found in GPC
+ * @return mixed Driver object or null if no such driver exists.
+ */
+ public function get_driver_by_gpc($quiet = false)
+ {
+ $this->load_drivers();
+ $driver_name = null;
+ foreach(array("_driver", "driver") as $input_name)
+ {
+ $driver_name = get_input_value($input_name, RCUBE_INPUT_GPC);
+ if($driver_name != null) break;
+ }
+
+ // Remove possible postfix "_driver" from requested driver name.
+ $driver_name = str_replace("_driver", "", $driver_name);
+
+ if($driver_name != null)
+ {
+ if(isset($this->_drivers[$driver_name]))
+ {
+ return $this->_drivers[$driver_name];
+ }
+ else
+ {
+ rcube::raise_error("Unknown driver requested \"$driver_name\".", true, true);
+ }
+ }
+ else
+ {
+ if(!$quiet) {
+ rcube::raise_error("No driver name found in GPC.", true, true);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper function to retrieve the default driver
+ *
+ * @return mixed Driver object or null if no default driver could be determined.
+ */
+ public function get_default_driver()
+ {
+ $default = $this->rc->config->get("calendar_driver_default", "kolab"); // Fallback to kolab if nothing was configured.
+ return $this->get_driver_by_name($default);
+ }
+
+ /**
+ * Return the driver for the given event.
+ *
+ * @param $id ID or UID of the event.
+ * @return mixed Returns the driver object or null if no driver could be found for this event.
+ */
+ public function get_driver_by_event($id)
+ {
+ foreach($this->get_drivers() as $driver) {
+ if($driver->get_event($id))
+ return $driver;
+ }
+
+ return null;
+ }
+
+ /**
+ * Get driver for given calendar id.
+ * @param int Calendar id to get driver for.
+ * @return mixed Driver object for given calendar.
+ */
+ public function get_driver_by_cal($cal_id)
+ {
+ if ($this->_cal_driver_map == null)
+ $this->get_calendars();
+
+ if (!isset($this->_cal_driver_map[$cal_id])){
+ rcube::raise_error("No driver found for calendar \"$cal_id\".", true, true);
+ }
+
+ return $this->_cal_driver_map[$cal_id];
+ }
+
+ /**
+ * Helper function to build calendar to driver map and calendar array.
+ * @return array List of calendar properties.
+ */
+ public function get_calendars()
+ {
+ if ($this->_cals == null || $this->_cal_driver_map == null) {
+ $this->_cals = array();
+ $this->_cal_driver_map = array();
+
+ $this->load_drivers();
+ foreach ($this->get_drivers() as $driver) {
+ foreach ((array)$driver->list_calendars() as $id => $prop) {
+ $prop["driver"] = get_class($driver);
+ $this->_cals[$id] = $prop;
+ $this->_cal_driver_map[$id] = $driver;
+ }
+ }
+ }
+
+ return $this->_cals;
+ }
+
+ /**
+ * Load iTIP functions
+ */
+ private function load_itip()
+ {
+ if (!$this->itip) {
+ require_once($this->home . '/lib/calendar_itip.php');
+ $this->itip = new calendar_itip($this);
+
+ if ($this->rc->config->get('kolab_invitation_calendars'))
+ $this->itip->set_rsvp_actions(array('accepted','tentative','declined','delegated','needs-action'));
+ }
+
+ return $this->itip;
+ }
+
+ /**
+ * Load iCalendar functions
+ */
+ public function get_ical()
+ {
+ if (!$this->ical) {
+ $this->ical = libcalendaring::get_ical();
+ }
+
+ return $this->ical;
+ }
+
+ /**
+ * Get properties of the calendar this user has specified as default
+ */
+ public function get_default_calendar($sensitivity = null)
+ {
+ $default_id = $this->rc->config->get('calendar_default_calendar');
+ // TODO: $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_WRITEABLE);
+
+ foreach($this->get_drivers() as $driver){
+ $calendars = $driver->list_calendars(false, true);
+ if($default_id) {
+ $calendar = $calendars[$default_id] ?: null;
+
+ if($calendar && (!$writeable || !$calendar["readonly"])
+ && (!$confidential || $calendar["subtype"] != "confidential"))
+ {
+ //rcmail::console("422: get_default_calendar(): " . print_r($calendar, true));
+ return $calendar;
+ }
+ }
+ else
+ {
+ // No default if, so get first calendar of first driver.
+ foreach ($calendars as $calendar) {
+ if ($calendar['default']) {
+ //rcmail::console("431: get_default_calendar(): " . print_r($calendar, true));
+ return $calendar;
+ }
+ if ((!$writeable || !$calendar['readonly']) && (!$confidential || $calendar["subtype"] != "confidential")) {
+ //rcmail::console("435: get_default_calendar(): " . print_r($calendar, true));
+ return $calendar;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Render the main calendar view from skin template
+ */
+ function calendar_view()
+ {
+ $this->rc->output->set_pagetitle($this->gettext('calendar'));
+
+ // Add CSS stylesheets to the page header
+ $this->ui->addCSS();
+
+ // Add JS files to the page header
+ $this->ui->addJS();
+
+ $this->ui->init_templates();
+ $this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning','close');
+ $this->rc->output->add_label('libcalendaring.itipaccepted','libcalendaring.itiptentative','libcalendaring.itipdeclined','libcalendaring.itipdelegated','libcalendaring.expandattendeegroup','libcalendaring.expandattendeegroupnodata');
+
+ // initialize attendees autocompletion
+ rcube_autocomplete_init();
+
+ $this->rc->output->set_env('timezone', $this->timezone->getName());
+ $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
+ $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver'));
+ $this->rc->output->set_env('mscolors', jqueryui::get_color_values());
+ $this->rc->output->set_env('identities-selector', $this->ui->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer'))));
+
+ $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
+ if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
+ $this->rc->output->set_env('view', $view);
+
+ if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC))
+ $this->rc->output->set_env('date', $date);
+
+ if ($msgref = rcube_utils::get_input_value('itip', rcube_utils::INPUT_GPC))
+ $this->rc->output->set_env('itip_events', $this->itip_events($msgref));
+
+ $this->rc->output->send("calendar.calendar");
+ }
+
+ /**
+ * Handler for preferences_sections_list hook.
+ * Adds Calendar settings sections into preferences sections list.
+ *
+ * @param array Original parameters
+ * @return array Modified parameters
+ */
+ function preferences_sections_list($p)
+ {
+ $p['list']['calendar'] = array(
+ 'id' => 'calendar', 'section' => $this->gettext('calendar'),
+ );
+
+ return $p;
+ }
+
+ /**
+ * Handler for preferences_list hook.
+ * Adds options blocks into Calendar settings sections in Preferences.
+ *
+ * @param array Original parameters
+ * @return array Modified parameters
+ */
+ function preferences_list($p)
+ {
+ if ($p['section'] != 'calendar') {
+ return $p;
+ }
+
+ $no_override = array_flip((array)$this->rc->config->get('dont_override'));
+
+ $p['blocks']['view']['name'] = $this->gettext('mainoptions');
+
+ if (!isset($no_override['calendar_default_view'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_default_view';
+ $select = new html_select(array('name' => '_default_view', 'id' => $field_id));
+ $select->add($this->gettext('day'), "agendaDay");
+ $select->add($this->gettext('week'), "agendaWeek");
+ $select->add($this->gettext('month'), "month");
+ $select->add($this->gettext('agenda'), "table");
+ $p['blocks']['view']['options']['default_view'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('default_view'))),
+ 'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
+ );
+ }
+
+ if (!isset($no_override['calendar_timeslots'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_timeslot';
+ $choices = array('1', '2', '3', '4', '6');
+ $select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
+ $select->add($choices);
+ $p['blocks']['view']['options']['timeslots'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('timeslots'))),
+ 'content' => $select->show(strval($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']))),
+ );
+ }
+
+ if (!isset($no_override['calendar_first_day'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_firstday';
+ $select = new html_select(array('name' => '_first_day', 'id' => $field_id));
+ $select->add(rcube_label('sunday'), '0');
+ $select->add(rcube_label('monday'), '1');
+ $select->add(rcube_label('tuesday'), '2');
+ $select->add(rcube_label('wednesday'), '3');
+ $select->add(rcube_label('thursday'), '4');
+ $select->add(rcube_label('friday'), '5');
+ $select->add(rcube_label('saturday'), '6');
+ $p['blocks']['view']['options']['first_day'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('first_day'))),
+ 'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
+ );
+ }
+
+ if (!isset($no_override['calendar_first_hour'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
+ $select_hours = new html_select();
+ for ($h = 0; $h < 24; $h++)
+ $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
+
+ $field_id = 'rcmfd_firsthour';
+ $p['blocks']['view']['options']['first_hour'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('first_hour'))),
+ 'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
+ );
+ }
+
+ if (!isset($no_override['calendar_work_start'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_workstart';
+ $p['blocks']['view']['options']['workinghours'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('workinghours'))),
+ 'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
+ ' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
+ );
+ }
+
+ if (!isset($no_override['calendar_event_coloring'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_coloring';
+ $select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
+ $select_colors->add($this->gettext('coloringmode0'), 0);
+ $select_colors->add($this->gettext('coloringmode1'), 1);
+ $select_colors->add($this->gettext('coloringmode2'), 2);
+ $select_colors->add($this->gettext('coloringmode3'), 3);
+
+ $p['blocks']['view']['options']['eventcolors'] = array(
+ 'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
+ 'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
+ );
+ }
+
+ if (!isset($no_override['calendar_default_alarm_type'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_alarm';
+ $select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
+ $select_type->add($this->gettext('none'), '');
+ $types = array();
+ foreach ($this->get_drivers() as $driver) {
+ foreach ($driver->alarm_types as $type) {
+ $types[$type] = $type;
+ }
+ }
+ foreach ($types as $type) {
+ $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
+ }
+ $p['blocks']['view']['options']['alarmtype'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
+ 'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
+ );
+ }
+
+ if (!isset($no_override['calendar_default_alarm_offset'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_alarm';
+ $input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
+ $select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
+ foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
+ $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
+
+ $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
+ $p['blocks']['view']['options']['alarmoffset'] = array(
+ 'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
+ 'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
+ );
+ }
+
+ if (!isset($no_override['calendar_default_calendar'])) {
+ if (!$p['current']) {
+ $p['blocks']['view']['content'] = true;
+ return $p;
+ }
+ // default calendar selection
+ $field_id = 'rcmfd_default_calendar';
+ $select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id, 'is_escaped' => true));
+ foreach($this->get_drivers() as $driver){
+ foreach ((array)$driver->list_calendars(false, true) as $id => $prop) {
+ $select_cal->add($prop['name'], strval($id));
+ if ($prop['default'])
+ $default_calendar = $id;
+ }
+ }
+ $p['blocks']['view']['options']['defaultcalendar'] = array(
+ 'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
+ 'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', $default_calendar)),
+ );
+ }
+
+ $p['blocks']['itip']['name'] = $this->gettext('itipoptions');
+
+ // Invitations handling
+ if (!isset($no_override['calendar_itip_after_action'])) {
+ if (!$p['current']) {
+ $p['blocks']['itip']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_after_action';
+ $select = new html_select(array('name' => '_after_action', 'id' => $field_id,
+ 'onchange' => "\$('#{$field_id}_select')[this.value == 4 ? 'show' : 'hide']()"));
+
+ $select->add($this->gettext('afternothing'), '');
+ $select->add($this->gettext('aftertrash'), 1);
+ $select->add($this->gettext('afterdelete'), 2);
+ $select->add($this->gettext('afterflagdeleted'), 3);
+ $select->add($this->gettext('aftermoveto'), 4);
+
+ $val = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
+ if ($val !== null && $val !== '' && !is_int($val)) {
+ $folder = $val;
+ $val = 4;
+ }
+
+ $folders = $this->rc->folder_selector(array(
+ 'id' => $field_id . '_select',
+ 'name' => '_after_action_folder',
+ 'maxlength' => 30,
+ 'folder_filter' => 'mail',
+ 'folder_rights' => 'w',
+ 'style' => $val !== 4 ? 'display:none' : '',
+ ));
+
+ $p['blocks']['itip']['options']['after_action'] = array(
+ 'title' => html::label($field_id, Q($this->gettext('afteraction'))),
+ 'content' => $select->show($val) . $folders->show($folder),
+ );
+ }
+
+ // category definitions
+ foreach ($this->get_drivers() as $driver) {
+ if (!$driver->nocategories && !isset($no_override['calendar_categories'])) {
+ $p['blocks']['categories']['name'] = $this->gettext('categories');
+
+ if (!$p['current']) {
+ $p['blocks']['categories']['content'] = true;
+ return $p;
+ }
+
+ $categories = (array)$driver->list_categories();
+ $categories_list = '';
+ foreach ($categories as $name => $color) {
+ $key = md5($name);
+ $field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
+ $category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
+ $category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $driver->categoriesimmutable));
+ $category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
+ $hidden = $driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
+ $categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
+ }
+
+ $p['blocks']['categories']['options']['category_' . $name] = array(
+ 'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
+ );
+
+ $field_id = 'rcmfd_new_category';
+ $new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
+ $add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
+ $p['blocks']['categories']['options']['categories'] = array(
+ 'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
+ );
+
+ $this->rc->output->add_script('function rcube_calendar_add_category(){
+ var name = $("#rcmfd_new_category").val();
+ if (name.length) {
+ var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
+ var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
+ var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
+ $("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
+ color.miniColors({ colorValues:(rcmail.env.mscolors || []) });
+ $("#rcmfd_new_category").val("");
+ }
+ }');
+
+ $this->rc->output->add_script('$("#rcmfd_new_category").keypress(function(event){
+ if (event.which == 13) {
+ rcube_calendar_add_category();
+ event.preventDefault();
+ }
+ });
+ ', 'docready');
+
+ // load miniColors js/css files
+ jqueryui::miniColors();
+ }
+ }
+
+ // virtual birthdays calendar
+ if (!isset($no_override['calendar_contact_birthdays'])) {
+ $p['blocks']['birthdays']['name'] = $this->gettext('birthdayscalendar');
+
+ if (!$p['current']) {
+ $p['blocks']['birthdays']['content'] = true;
+ return $p;
+ }
+
+ $field_id = 'rcmfd_contact_birthdays';
+ $input = new html_checkbox(array('name' => '_contact_birthdays', 'id' => $field_id, 'value' => 1, 'onclick' => '$(".calendar_birthday_props").prop("disabled",!this.checked)'));
+
+ $p['blocks']['birthdays']['options']['contact_birthdays'] = array(
+ 'title' => html::label($field_id, $this->gettext('displaybirthdayscalendar')),
+ 'content' => $input->show($this->rc->config->get('calendar_contact_birthdays')?1:0),
+ );
+
+ $input_attrib = array(
+ 'class' => 'calendar_birthday_props',
+ 'disabled' => !$this->rc->config->get('calendar_contact_birthdays'),
+ );
+
+ $sources = array();
+ $checkbox = new html_checkbox(array('name' => '_birthday_adressbooks[]') + $input_attrib);
+ foreach ($this->rc->get_address_sources(false, true) as $source) {
+ $active = in_array($source['id'], (array)$this->rc->config->get('calendar_birthday_adressbooks', array())) ? $source['id'] : '';
+ $sources[] = html::label(null, $checkbox->show($active, array('value' => $source['id'])) . '&nbsp;' . rcube::Q($source['realname'] ?: $source['name']));
+ }
+
+ $p['blocks']['birthdays']['options']['birthday_adressbooks'] = array(
+ 'title' => rcube::Q($this->gettext('birthdayscalendarsources')),
+ 'content' => join(html::br(), $sources),
+ );
+
+ $field_id = 'rcmfd_birthdays_alarm';
+ $select_type = new html_select(array('name' => '_birthdays_alarm_type', 'id' => $field_id) + $input_attrib);
+ $select_type->add($this->gettext('none'), '');
+ foreach ($this->get_default_driver()->alarm_types as $type) { // TODO: Replace with dedicated birthday calendar as soon as it is available
+ $select_type->add(rcube_label(strtolower("alarm{$type}option"), 'libcalendaring'), $type);
+ }
+
+ $input_value = new html_inputfield(array('name' => '_birthdays_alarm_value', 'id' => $field_id . 'value', 'size' => 3) + $input_attrib);
+ $select_offset = new html_select(array('name' => '_birthdays_alarm_offset', 'id' => $field_id . 'offset') + $input_attrib);
+ foreach (array('-M','-H','-D') as $trigger)
+ $select_offset->add(rcube_label('trigger' . $trigger, 'libcalendaring'), $trigger);
+
+ $preset = libcalendaring::parse_alarm_value($this->rc->config->get('calendar_birthdays_alarm_offset', '-1D'));
+ $p['blocks']['birthdays']['options']['birthdays_alarmoffset'] = array(
+ 'title' => html::label($field_id . 'value', rcube::Q($this->gettext('showalarms'))),
+ 'content' => $select_type->show($this->rc->config->get('calendar_birthdays_alarm_type', '')) . ' ' . $input_value->show($preset[0]) . '&nbsp;' . $select_offset->show($preset[1]),
+ );
+ }
+
+ return $p;
+ }
+
+ /**
+ * Handler for preferences_save hook.
+ * Executed on Calendar settings form submit.
+ *
+ * @param array Original parameters
+ * @return array Modified parameters
+ */
+ function preferences_save($p)
+ {
+ if ($p['section'] == 'calendar') {
+
+ // compose default alarm preset value
+ $alarm_offset = rcube_utils::get_input_value('_alarm_offset', rcube_utils::INPUT_POST);
+ $alarm_value = rcube_utils::get_input_value('_alarm_value', rcube_utils::INPUT_POST);
+ $default_alarm = $alarm_offset[0] . intval($alarm_value) . $alarm_offset[1];
+
+ $birthdays_alarm_offset = rcube_utils::get_input_value('_birthdays_alarm_offset', rcube_utils::INPUT_POST);
+ $birthdays_alarm_value = rcube_utils::get_input_value('_birthdays_alarm_value', rcube_utils::INPUT_POST);
+ $birthdays_alarm_value = $birthdays_alarm_offset[0] . intval($birthdays_alarm_value) . $birthdays_alarm_offset[1];
+
+ $p['prefs'] = array(
+ 'calendar_default_view' => rcube_utils::get_input_value('_default_view', rcube_utils::INPUT_POST),
+ 'calendar_timeslots' => intval(rcube_utils::get_input_value('_timeslots', rcube_utils::INPUT_POST)),
+ 'calendar_first_day' => intval(rcube_utils::get_input_value('_first_day', rcube_utils::INPUT_POST)),
+ 'calendar_first_hour' => intval(rcube_utils::get_input_value('_first_hour', rcube_utils::INPUT_POST)),
+ 'calendar_work_start' => intval(rcube_utils::get_input_value('_work_start', rcube_utils::INPUT_POST)),
+ 'calendar_work_end' => intval(rcube_utils::get_input_value('_work_end', rcube_utils::INPUT_POST)),
+ 'calendar_event_coloring' => intval(rcube_utils::get_input_value('_event_coloring', rcube_utils::INPUT_POST)),
+ 'calendar_default_alarm_type' => rcube_utils::get_input_value('_alarm_type', rcube_utils::INPUT_POST),
+ 'calendar_default_alarm_offset' => $default_alarm,
+ 'calendar_default_calendar' => rcube_utils::get_input_value('_default_calendar', rcube_utils::INPUT_POST),
+ 'calendar_date_format' => null, // clear previously saved values
+ 'calendar_time_format' => null,
+ 'calendar_contact_birthdays' => rcube_utils::get_input_value('_contact_birthdays', rcube_utils::INPUT_POST) ? true : false,
+ 'calendar_birthday_adressbooks' => (array) rcube_utils::get_input_value('_birthday_adressbooks', rcube_utils::INPUT_POST),
+ 'calendar_birthdays_alarm_type' => rcube_utils::get_input_value('_birthdays_alarm_type', rcube_utils::INPUT_POST),
+ 'calendar_birthdays_alarm_offset' => $birthdays_alarm_value ?: null,
+ 'calendar_itip_after_action' => intval(rcube_utils::get_input_value('_after_action', rcube_utils::INPUT_POST)),
+ );
+
+ if ($p['prefs']['calendar_itip_after_action'] == 4) {
+ $p['prefs']['calendar_itip_after_action'] = rcube_utils::get_input_value('_after_action_folder', rcube_utils::INPUT_POST, true);
+ }
+
+ // categories
+ foreach($this->get_drivers() as $driver) {
+ if (!$driver->nocategories) {
+ $old_categories = $new_categories = array();
+ foreach ($driver->list_categories() as $name => $color) {
+ $old_categories[md5($name)] = $name;
+ }
+
+ $categories = (array)rcube_utils::get_input_value('_categories', rcube_utils::INPUT_POST);
+ $colors = (array)rcube_utils::get_input_value('_colors', rcube_utils::INPUT_POST);
+
+ foreach ($categories as $key => $name) {
+ $color = preg_replace('/^#/', '', strval($colors[$key]));
+
+ // rename categories in existing events -> driver's job
+ if ($oldname = $old_categories[$key]) {
+ $driver->replace_category($oldname, $name, $color);
+ unset($old_categories[$key]);
+ } else
+ $driver->add_category($name, $color);
+
+ $new_categories[$name] = $color;
+ }
+
+ // these old categories have been removed, alter events accordingly -> driver's job
+ foreach ((array)$old_categories[$key] as $key => $name) {
+ $driver->remove_category($name);
+ }
+
+ $p['prefs']['calendar_categories'] = $new_categories;
+ }
+ }
+ }
+
+ return $p;
+ }
+
+ /**
+ * Dispatcher for calendar actions initiated by the client
+ */
+ function calendar_action()
+ {
+ $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
+ $cal = rcube_utils::get_input_value('c', rcube_utils::INPUT_GPC);
+ $success = $reload = false;
+ $driver = null;
+
+ if (isset($cal['showalarms']))
+ $cal['showalarms'] = intval($cal['showalarms']);
+
+ switch ($action) {
+ case "form-new":
+ case "form-edit":
+ echo $this->ui->calendar_editform($action, $cal);
+ exit;
+ case "new":
+ $driver = $this->get_driver_by_gpc();
+ $success = $driver->create_calendar($cal);
+ $reload = true;
+ break;
+ case "edit":
+ $driver = $this->get_driver_by_cal($cal['id']);
+ $success = $driver->edit_calendar($cal);
+ $reload = true;
+ break;
+ case "delete":
+ $driver = $this->get_driver_by_cal($cal['id']);
+ if ($success = $driver->delete_calendar($cal))
+ $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
+ break;
+ case "subscribe":
+ $driver = $this->get_driver_by_cal($cal['id']);
+ if (!$driver->subscribe_calendar($cal))
+ $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
+ return;
+ case "search":
+ $results = array();
+ $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
+ $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
+ $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
+
+ $search_more_results = false;
+ foreach($this->get_drivers() as $driver) {
+ foreach ((array)$driver->search_calendars($query, $source) as $id => $prop) {
+ $editname = $prop['editname'];
+ unset($prop['editname']); // force full name to be displayed
+ $prop['active'] = false;
+
+ // let the UI generate HTML and CSS representation for this calendar
+ $html = $this->ui->calendar_list_item($id, $prop, $jsenv);
+ $cal = $jsenv[$id];
+ $cal['editname'] = $editname;
+ $cal['html'] = $html;
+ if (!empty($prop['color']))
+ $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);
+
+ $results[] = $cal;
+ }
+
+ $search_more_results |= $driver->search_more_results;
+ }
+
+ // report more results available
+ if ($search_more_results)
+ $this->rc->output->show_message('autocompletemore', 'info');
+
+ $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
+ return;
+ }
+
+ if ($success)
+ $this->rc->output->show_message('successfullysaved', 'confirmation');
+ else {
+ $error_msg = $this->gettext('errorsaving') . ($driver && $driver->last_error ? ': ' . $driver->last_error :'');
+ $this->rc->output->show_message($error_msg, 'error');
+ }
+
+ $this->rc->output->command('plugin.unlock_saving');
+
+ if ($success && $reload)
+ $this->rc->output->command('plugin.reload_view');
+ }
+
+
+ /**
+ * Dispatcher for event actions initiated by the client
+ */
+ function event_action()
+ {
+ $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
+ $event = rcube_utils::get_input_value('e', rcube_utils::INPUT_POST, true);
+ $success = $reload = $got_msg = false;
+
+ $driver = null;
+ if($event['calendar'])
+ $driver = $this->get_driver_by_cal($event['calendar']);
+
+ // This can happen if creating a new event outside the calendar e.g. from an ical file attached to an email.
+ if(!$driver)
+ $driver = $this->get_default_driver();
+
+ // force notify if hidden + active
+ if ((int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']) === 1)
+ $event['_notify'] = 1;
+
+ // read old event data in order to find changes
+ if (($event['_notify'] || $event['_decline']) && $action != 'new') {
+ $old = $this->driver->get_event($event);
+
+ // Support event moving across different drivers
+ if(isset($event["_fromcalendar"]) && $event["_fromcalendar"] != $event["calendar"]) {
+ $fromdriver = $this->get_driver_by_cal($event["_fromcalendar"]);
+ if(get_class($fromdriver) != get_class($driver)) {
+ $fromevent = $event;
+ $fromevent["calendar"] = $event["_fromcalendar"];
+ if($fromdriver->remove_event($fromevent))
+ $action = "new";
+ }
+ }
+
+ // load main event if savemode is 'all' or if deleting 'future' events
+ if (($event['_savemode'] == 'all' || ($event['_savemode'] == 'future' && $action == 'remove' && !$event['_decline'])) && $old['recurrence_id']) {
+ $old['id'] = $old['recurrence_id'];
+ $old = $this->driver->get_event($old);
+ }
+ }
+
+ switch ($action) {
+ case "new":
+ // create UID for new event
+ $event['uid'] = $this->generate_uid();
+ $this->write_preprocess($event, $action);
+ if ($success = $driver->new_event($event)) {
+ $event['id'] = $event['uid'];
+ $event['_savemode'] = 'all';
+ $this->cleanup_event($event);
+ $this->event_save_success($event, null, $action, true);
+ }
+ $reload = $success && $event['recurrence'] ? 2 : 1;
+ break;
+
+ case "edit":
+ $this->write_preprocess($event, $action);
+ if ($success = $driver->edit_event($event)) {
+ $this->cleanup_event($event);
+ $this->event_save_success($event, $old, $action, $success);
+ }
+ $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
+ break;
+
+ case "resize":
+ $this->write_preprocess($event, $action);
+ if ($success = $driver->resize_event($event)) {
+ $this->event_save_success($event, $old, $action, $success);
+ }
+ $reload = $event['_savemode'] ? 2 : 1;
+ break;
+
+ case "move":
+ $this->write_preprocess($event, $action);
+ if ($success = $driver->move_event($event)) {
+ $this->event_save_success($event, $old, $action, $success);
+ }
+ $reload = $success && $event['_savemode'] ? 2 : 1;
+ break;
+
+ case "remove":
+ // remove previous deletes
+ $undo_time = $driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
+ $this->rc->session->remove('calendar_event_undo');
+
+ // search for event if only UID is given
+ if (!isset($event['calendar']) && $event['uid']) {
+ if (!($event = $driver->get_event($event, calendar_driver::FILTER_WRITEABLE))) {
+ break;
+ }
+ $undo_time = 0;
+ }
+
+ $success = $driver->remove_event($event, $undo_time < 1);
+ $reload = (!$success || $event['_savemode']) ? 2 : 1;
+
+ if ($undo_time > 0 && $success) {
+ $_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
+ // display message with Undo link.
+ $msg = html::span(null, $this->gettext('successremoval'))
+ . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
+ JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
+ $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
+ $got_msg = true;
+ }
+ else if ($success) {
+ $this->rc->output->show_message('calendar.successremoval', 'confirmation');
+ $got_msg = true;
+ }
+
+ // send cancellation for the main event
+ if ($event['_savemode'] == 'all') {
+ unset($old['_instance'], $old['recurrence_date'], $old['recurrence_id']);
+ }
+ // send an update for the main event's recurrence rule instead of a cancellation message
+ else if ($event['_savemode'] == 'future' && $success !== false && $success !== true) {
+ $event['_savemode'] = 'all'; // force event_save_success() to load master event
+ $action = 'edit';
+ $success = true;
+ }
+
+ // send iTIP reply that participant has declined the event
+ if ($success && $event['_decline']) {
+ $emails = $this->get_user_emails();
+ foreach ($old['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER')
+ $organizer = $attendee;
+ else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $old['attendees'][$i]['status'] = 'DECLINED';
+ $reply_sender = $attendee['email'];
+ }
+ }
+
+ if ($event['_savemode'] == 'future' && $event['id'] != $old['id']) {
+ $old['thisandfuture'] = true;
+ }
+
+ $itip = $this->load_itip();
+ $itip->set_sender_email($reply_sender);
+ if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ else if ($success) {
+ $this->event_save_success($event, $old, $action, $success);
+ }
+ break;
+
+ case "undo":
+ // Restore deleted event
+ $event = $_SESSION['calendar_event_undo']['data'];
+
+ if ($event)
+ $success = $driver->restore_event($event);
+
+ if ($success) {
+ $this->rc->session->remove('calendar_event_undo');
+ $this->rc->output->show_message('calendar.successrestore', 'confirmation');
+ $got_msg = true;
+ $reload = 2;
+ }
+
+ break;
+
+ case "rsvp":
+ $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+ $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_POST);
+ $attendees = rcube_utils::get_input_value('attendees', rcube_utils::INPUT_POST);
+ $reply_comment = $event['comment'];
+
+ $this->write_preprocess($event, 'edit');
+ $ev = $driver->get_event($event);
+ $ev['attendees'] = $event['attendees'];
+ $ev['free_busy'] = $event['free_busy'];
+ $ev['_savemode'] = $event['_savemode'];
+
+ // send invitation to delegatee + add it as attendee
+ if ($status == 'delegated' && $event['to']) {
+ $itip = $this->load_itip();
+ if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'], $attendees)) {
+ $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ $noreply = false;
+ }
+ }
+
+ $event = $ev;
+
+ // compose a list of attendees affected by this change
+ $updated_attendees = array_filter(array_map(function($j) use ($event) {
+ return $event['attendees'][$j];
+ }, $attendees));
+
+ if ($success = $driver->edit_rsvp($event, $status, $updated_attendees)) {
+ $noreply = rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC);
+ $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
+ $reload = $event['calendar'] != $ev['calendar'] || $event['recurrence'] ? 2 : 1;
+ $organizer = null;
+ $emails = $this->get_user_emails();
+
+ foreach ($event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $reply_sender = $attendee['email'];
+ }
+ }
+
+ if (!$noreply) {
+ $itip = $this->load_itip();
+ $itip->set_sender_email($reply_sender);
+ $event['comment'] = $reply_comment;
+ $event['thisandfuture'] = $event['_savemode'] == 'future';
+ if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+
+ // refresh all calendars
+ if ($event['calendar'] != $ev['calendar']) {
+ $this->rc->output->command('plugin.refresh_calendar', array('source' => null, 'refetch' => true));
+ $reload = 0;
+ }
+ }
+ break;
+
+ case "dismiss":
+ $event['ids'] = explode(',', $event['id']);
+ $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $event);
+ $success = $plugin['success'];
+ foreach ($event['ids'] as $id) {
+ if (strpos($id, 'cal:') === 0)
+ $success |= $driver->dismiss_alarm(substr($id, 4), $event['snooze']);
+ }
+ break;
+
+ case "changelog":
+ $data = $driver->get_event_changelog($event);
+ if (is_array($data) && !empty($data)) {
+ $lib = $this->lib;
+ $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
+ array_walk($data, function(&$change) use ($lib, $dtformat) {
+ if ($change['date']) {
+ $dt = $lib->adjust_timezone($change['date']);
+ if ($dt instanceof DateTime)
+ $change['date'] = $this->rc->format_date($dt, $dtformat, false);
+ }
+ });
+ $this->rc->output->command('plugin.render_event_changelog', $data);
+ }
+ else {
+ $this->rc->output->command('plugin.render_event_changelog', false);
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "diff":
+ $data = $driver->get_event_diff($event, $event['rev1'], $event['rev2']);
+ if (is_array($data)) {
+ // convert some properties, similar to self::_client_event()
+ $lib = $this->lib;
+ array_walk($data['changes'], function(&$change, $i) use ($event, $lib) {
+ // convert date cols
+ foreach (array('start','end','created','changed') as $col) {
+ if ($change['property'] == $col) {
+ $change['old'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format('c');
+ $change['new'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format('c');
+ }
+ }
+ // create textual representation for alarms and recurrence
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']))
+ $change['old_'] = libcalendaring::alarm_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'recurrence') {
+ if (is_array($change['old']))
+ $change['old_'] = $lib->recurrence_text($change['old']);
+ if (is_array($change['new']))
+ $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
+ }
+ if ($change['property'] == 'attachments') {
+ if (is_array($change['old']))
+ $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
+ if (is_array($change['new']))
+ $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
+ }
+ // compute a nice diff of description texts
+ if ($change['property'] == 'description') {
+ $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
+ }
+ });
+ $this->rc->output->command('plugin.event_show_diff', $data);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "show":
+ if ($event = $driver->get_event_revison($event, $event['rev'])) {
+ $this->rc->output->command('plugin.event_show_revision', $this->_client_event($event));
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
+ }
+ $got_msg = true;
+ $reload = false;
+ break;
+
+ case "restore":
+ if ($success = $driver->restore_event_revision($event, $event['rev'])) {
+ $_event = $driver->get_event($event);
+ $reload = $_event['recurrence'] ? 2 : 1;
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $event['rev']))), 'confirmation');
+ $this->rc->output->command('plugin.close_history_dialog');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
+ $reload = 0;
+ }
+ $got_msg = true;
+ break;
+ }
+
+ // show confirmation/error message
+ if (!$got_msg) {
+ if ($success)
+ $this->rc->output->show_message('successfullysaved', 'confirmation');
+ else
+ $this->rc->output->show_message('calendar.errorsaving', 'error');
+ }
+
+ // unlock client
+ $this->rc->output->command('plugin.unlock_saving');
+
+ // update event object on the client or trigger a complete refretch if too complicated
+ if ($reload) {
+ $args = array('source' => $event['calendar']);
+ if ($reload > 1)
+ $args['refetch'] = true;
+ else if ($success && $action != 'remove')
+ $args['update'] = $this->_client_event($driver->get_event($event), true);
+ $this->rc->output->command('plugin.refresh_calendar', $args);
+ }
+ }
+
+ /**
+ * Helper method sending iTip notifications after successful event updates
+ */
+ private function event_save_success(&$event, $old, $action, $success)
+ {
+ // $success is a new event ID
+ if ($success !== true) {
+ // send update notification on the main event
+ if ($event['_savemode'] == 'future' && $event['_notify'] && $old['attendees'] && $old['recurrence_id']) {
+ $master = $this->driver->get_event(array('id' => $old['recurrence_id'], 'calendar' => $old['calendar']), 0, true);
+ unset($master['_instance'], $master['recurrence_date']);
+
+ $sent = $this->notify_attendees($master, null, $action, $event['_comment']);
+ if ($sent < 0)
+ $this->rc->output->show_message('calendar.errornotifying', 'error');
+
+ $event['attendees'] = $master['attendees']; // this tricks us into the next if clause
+ }
+
+ // delete old reference if saved as new
+ if ($event['_savemode'] == 'future' || $event['_savemode'] == 'new') {
+ $old = null;
+ }
+
+ $event['id'] = $success;
+ $event['_savemode'] = 'all';
+ }
+
+ // send out notifications
+ if ($event['_notify'] && ($event['attendees'] || $old['attendees'])) {
+ $_savemode = $event['_savemode'];
+
+ // send notification for the main event when savemode is 'all'
+ if ($action != 'remove' && $_savemode == 'all' && ($event['recurrence_id'] || $old['recurrence_id'] || ($old && $old['id'] != $event['id']))) {
+ $event['id'] = $event['recurrence_id'] ?: ($old['recurrence_id'] ?: $old['id']);
+ $event = $this->driver->get_event($event, 0, true);
+ unset($event['_instance'], $event['recurrence_date']);
+ }
+ else {
+ // make sure we have the complete record
+ $event = $action == 'remove' ? $old : $this->driver->get_event($event, 0, true);
+ }
+
+ $event['_savemode'] = $_savemode;
+
+ if ($old) {
+ $old['thisandfuture'] = $_savemode == 'future';
+ }
+
+ // only notify if data really changed (TODO: do diff check on client already)
+ if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
+ $sent = $this->notify_attendees($event, $old, $action, $event['_comment']);
+ if ($sent > 0)
+ $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ else if ($sent < 0)
+ $this->rc->output->show_message('calendar.errornotifying', 'error');
+ }
+ }
+ }
+
+ /**
+ * Handler for load-requests from fullcalendar
+ * This will return pure JSON formatted output
+ */
+ function load_events()
+ {
+ $events = $this->get_driver_by_gpc()->load_events(
+ rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
+ rcube_utils::get_input_value('end', rcube_utils::INPUT_GET),
+ ($query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GET)),
+ rcube_utils::get_input_value('source', rcube_utils::INPUT_GET)
+ );
+ echo $this->encode($events, !empty($query));
+ exit;
+ }
+
+ /**
+ * Handler for requests fetching event counts for calendars
+ */
+ public function count_events()
+ {
+ // don't update session on these requests (avoiding race conditions)
+ $this->rc->session->nowrite = true;
+
+ $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
+ if (!$start) {
+ $start = new DateTime('today 00:00:00', $this->timezone);
+ $start = $start->format('U');
+ }
+
+ $counts = 0;
+ foreach($this->get_drivers() as $driver) {
+ $counts += $driver->count_events(
+ rcube_utils::get_input_value('source', rcube_utils::INPUT_GET),
+ $start,
+ rcube_utils::get_input_value('end', rcube_utils::INPUT_GET)
+ );
+ }
+
+ $this->rc->output->command('plugin.update_counts', array('counts' => $counts));
+ }
+
+ /**
+ * Load event data from an iTip message attachment
+ */
+ public function itip_events($msgref)
+ {
+ $path = explode('/', $msgref);
+ $msg = array_pop($path);
+ $mbox = join('/', $path);
+ list($uid, $mime_id) = explode('#', $msg);
+ $events = array();
+
+ if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
+ $partstat = 'NEEDS-ACTION';
+/*
+ $user_emails = $this->lib->get_user_emails();
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails)) {
+ $partstat = $attendee['status'];
+ break;
+ }
+ }
+*/
+ $event['id'] = $event['uid'];
+ $event['temporary'] = true;
+ $event['readonly'] = true;
+ $event['calendar'] = '--invitation--itip';
+ $event['className'] = 'fc-invitation-' . strtolower($partstat);
+ $event['_mbox'] = $mbox;
+ $event['_uid'] = $uid;
+ $event['_part'] = $mime_id;
+
+ $events[] = $this->_client_event($event, true);
+
+ // add recurring instances
+ if (!empty($event['recurrence'])) {
+ foreach ($this->driver->get_recurring_events($event, $event['start']) as $recurring) {
+ $recurring['temporary'] = true;
+ $recurring['readonly'] = true;
+ $recurring['calendar'] = '--invitation--itip';
+ $events[] = $this->_client_event($recurring, true);
+ }
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Handler for keep-alive requests
+ * This will check for updated data in active calendars and sync them to the client
+ */
+ public function refresh($attr)
+ {
+ // refresh the entire calendar every 10th time to also sync deleted events
+ if (rand(0,10) == 10) {
+ $this->rc->output->command('plugin.refresh_calendar', array('refetch' => true));
+ return;
+ }
+
+ $counts = array();
+
+ foreach($this->get_drivers() as $driver) {
+ foreach ($driver->list_calendars(calendar_driver::FILTER_ACTIVE) as $cal) {
+ $events = $driver->load_events(
+ rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC),
+ rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC),
+ rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
+ $cal['id'],
+ 1,
+ $attr['last']
+ );
+
+ foreach ($events as $event) {
+ $this->rc->output->command('plugin.refresh_calendar',
+ array('source' => $cal['id'], 'update' => $this->_client_event($event)));
+ }
+
+ // refresh count for this calendar
+ if ($cal['counts']) {
+ $today = new DateTime('today 00:00:00', $this->timezone);
+ $counts += $driver->count_events($cal['id'], $today->format('U'));
+ }
+ }
+ }
+
+ if (!empty($counts)) {
+ $this->rc->output->command('plugin.update_counts', array('counts' => $counts));
+ }
+ }
+
+ /**
+ * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
+ * This will check for pending notifications and pass them to the client
+ */
+ public function pending_alarms($p)
+ {
+ $time = $p['time'] ?: time();
+ foreach($this->get_drivers() as $driver) {
+ if ($alarms = $driver->pending_alarms($time)) {
+ foreach ($alarms as $alarm) {
+ $alarm['id'] = 'cal:' . $alarm['id']; // prefix ID with cal:
+ $p['alarms'][] = $alarm;
+ }
+ }
+ }
+
+ // get alarms for birthdays calendar
+ if ($this->rc->config->get('calendar_contact_birthdays') && $this->rc->config->get('calendar_birthdays_alarm_type') == 'DISPLAY') {
+ $cache = $this->rc->get_cache('calendar.birthdayalarms', 'db');
+
+ // TODO: Use dedicated birthday calendar as soon as it exists
+ foreach ($this->get_default_driver()->load_birthday_events($time, $time + 86400 * 60) as $e) {
+ $alarm = libcalendaring::get_next_alarm($e);
+
+ // overwrite alarm time with snooze value (or null if dismissed)
+ if ($dismissed = $cache->get($e['id']))
+ $alarm['time'] = $dismissed['notifyat'];
+
+ // add to list if alarm is set
+ if ($alarm && $alarm['time'] && $alarm['time'] <= $time) {
+ $e['id'] = 'cal:bday:' . $e['id'];
+ $e['notifyat'] = $alarm['time'];
+ $p['alarms'][] = $e;
+ }
+ }
+ }
+
+ return $p;
+ }
+
+ /**
+ * Handler for alarm dismiss hook triggered by libcalendaring
+ */
+ public function dismiss_alarms($p)
+ {
+ foreach($this->get_drivers() as $driver) { // TODO: Maybe use get_driver_by_cal() ?
+ foreach ((array)$p['ids'] as $id) {
+ if (strpos($id, 'cal:bday:') === 0) {
+ $p['success'] |= $driver->dismiss_birthday_alarm(substr($id, 9), $p['snooze']);
+ } else if (strpos($id, 'cal:') === 0) {
+ $p['success'] |= $driver->dismiss_alarm(substr($id, 4), $p['snooze']);
+ }
+ }
+ }
+
+ return $p;
+ }
+
+ /**
+ * Handler for check-recent requests which are accidentally sent to calendar taks
+ */
+ function check_recent()
+ {
+ // NOP
+ $this->rc->output->send();
+ }
+
+ /**
+ * Hook triggered when a contact is saved
+ */
+ function contact_update($p)
+ {
+ // clear birthdays calendar cache
+ if (!empty($p['record']['birthday'])) {
+ $cache = $this->rc->get_cache('calendar.birthdays', 'db');
+ $cache->remove();
+ }
+ }
+
+ /**
+ *
+ */
+ function import_events()
+ {
+ // Upload progress update
+ if (!empty($_GET['_progress'])) {
+ rcube_upload_progress();
+ }
+
+ @set_time_limit(0);
+
+ // process uploaded file if there is no error
+ $err = $_FILES['_data']['error'];
+
+ if (!$err && $_FILES['_data']['tmp_name']) {
+ $calendar = rcube_utils::get_input_value('calendar', rcube_utils::INPUT_GPC);
+ $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
+
+ // extract zip file
+ if ($_FILES['_data']['type'] == 'application/zip') {
+ $count = 0;
+ if (class_exists('ZipArchive', false)) {
+ $zip = new ZipArchive();
+ if ($zip->open($_FILES['_data']['tmp_name'])) {
+ $randname = uniqid('zip-' . session_id(), true);
+ $tmpdir = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;
+ mkdir($tmpdir, 0700);
+
+ // extract each ical file from the archive and import it
+ for ($i = 0; $i < $zip->numFiles; $i++) {
+ $filename = $zip->getNameIndex($i);
+ if (preg_match('/\.ics$/i', $filename)) {
+ $tmpfile = $tmpdir . '/' . basename($filename);
+ if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) {
+ $count += $this->import_from_file($tmpfile, $calendar, $rangestart, $errors);
+ unlink($tmpfile);
+ }
+ }
+ }
+
+ rmdir($tmpdir);
+ $zip->close();
+ }
+ else {
+ $errors = 1;
+ $msg = 'Failed to open zip file.';
+ }
+ }
+ else {
+ $errors = 1;
+ $msg = 'Zip files are not supported for import.';
+ }
+ }
+ else {
+ // attempt to import teh uploaded file directly
+ $count = $this->import_from_file($_FILES['_data']['tmp_name'], $calendar, $rangestart, $errors);
+ }
+
+ if ($count) {
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
+ $this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
+ }
+ else if (!$errors) {
+ $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
+ $this->rc->output->command('plugin.import_success', array('source' => $calendar));
+ }
+ else {
+ $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
+ }
+ }
+ else {
+ if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+ $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
+ 'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+ }
+ else {
+ $msg = rcube_label('fileuploaderror');
+ }
+
+ $this->rc->output->command('plugin.import_error', array('message' => $msg));
+ }
+
+ $this->rc->output->send('iframe');
+ }
+
+ /**
+ * Helper function to parse and import a single .ics file
+ */
+ private function import_from_file($filepath, $calendar, $rangestart, &$errors)
+ {
+ $user_email = $this->rc->user->get_username();
+
+ $ical = $this->get_ical();
+ $errors = !$ical->fopen($filepath);
+ $count = $i = 0;
+ $driver = $this->get_driver_by_cal($calendar);
+ foreach ($ical as $event) {
+ // keep the browser connection alive on long import jobs
+ if (++$i > 100 && $i % 100 == 0) {
+ echo "<!-- -->";
+ ob_flush();
+ }
+
+ // TODO: correctly handle recurring events which start before $rangestart
+ if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
+ continue;
+
+ $event['_owner'] = $user_email;
+ $event['calendar'] = $calendar;
+ if ($driver->new_event($event)) {
+ $count++;
+ }
+ else {
+ $errors++;
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Construct the ics file for exporting events to iCalendar format;
+ */
+ function export_events($terminate = true)
+ {
+ $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GET);
+ $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GET);
+
+ if (!isset($start))
+ $start = 'today -1 year';
+ if (!is_numeric($start))
+ $start = strtotime($start . ' 00:00:00');
+ if (!$end)
+ $end = 'today +10 years';
+ if (!is_numeric($end))
+ $end = strtotime($end . ' 23:59:59');
+
+ $event_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
+ $attachments = rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GET);
+ $calid = $filename = rcube_utils::get_input_value('source', rcube_utils::INPUT_GET);
+ $driver = $this->get_driver_by_cal($calid);
+ $calendars = $this->driver->list_calendars();
+ $events = array();
+
+ if ($calendars[$calid]) {
+ $filename = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
+ $filename = asciiwords(html_entity_decode($filename)); // to 7bit ascii
+ if (!empty($event_id)) {
+ if ($event = $driver->get_event(array('calendar' => $calid, 'id' => $event_id), 0, true)) {
+ if ($event['recurrence_id']) {
+ $event = $driver->get_event(array('calendar' => $calid, 'id' => $event['recurrence_id']), 0, true);
+ }
+ $events = array($event);
+ $filename = asciiwords($event['title']);
+ if (empty($filename))
+ $filename = 'event';
+ }
+ }
+ else {
+ $events = $driver->load_events($start, $end, null, $calid, 0);
+ if (empty($filename))
+ $filename = $calid;
+ }
+ }
+
+ header("Content-Type: text/calendar");
+ header("Content-Disposition: inline; filename=".$filename.'.ics');
+
+ $this->get_ical()->export($events, '', true, $attachments ? array($driver, 'get_attachment_body') : null);
+
+ if ($terminate)
+ exit;
+ }
+
+ /**
+ * Handler for iCal feed requests
+ */
+ function ical_feed_export()
+ {
+ $session_exists = !empty($_SESSION['user_id']);
+
+ // process HTTP auth info
+ if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
+ $_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
+ $auth = $this->rc->plugins->exec_hook('authenticate', array(
+ 'host' => $this->rc->autoselect_host(),
+ 'user' => trim($_SERVER['PHP_AUTH_USER']),
+ 'pass' => $_SERVER['PHP_AUTH_PW'],
+ 'cookiecheck' => true,
+ 'valid' => true,
+ ));
+ if ($auth['valid'] && !$auth['abort'])
+ $this->rc->login($auth['user'], $auth['pass'], $auth['host']);
+ }
+
+ // require HTTP auth
+ if (empty($_SESSION['user_id'])) {
+ header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
+ header('HTTP/1.0 401 Unauthorized');
+ exit;
+ }
+
+ // decode calendar feed hash
+ $format = 'ics';
+ $calhash = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GET);
+ if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
+ $format = strtolower($m[1]);
+ $calhash = preg_replace($suff_regex, '', $calhash);
+ }
+
+ if (!strpos($calhash, ':'))
+ $calhash = base64_decode($calhash);
+
+ list($user, $_GET['source']) = explode(':', $calhash, 2);
+
+ // sanity check user
+ if ($this->rc->user->get_username() == $user) {
+ $this->export_events(false);
+ }
+ else {
+ header('HTTP/1.0 404 Not Found');
+ }
+
+ // don't save session data
+ if (!$session_exists)
+ session_destroy();
+ exit;
+ }
+
+
+ /**
+ *
+ */
+ function load_settings()
+ {
+ $this->lib->load_settings();
+ $this->defaults += $this->lib->defaults;
+
+ $settings = array();
+
+ // configuration
+ $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
+ $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
+ $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
+
+ $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
+ $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
+ $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
+ $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
+ $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
+ $settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
+ $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
+ $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
+ $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
+ $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+ $settings['invitation_calendars'] = (bool)$this->rc->config->get('kolab_invitation_calendars', false);
+ $settings['itip_notify'] = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+
+ // get user identity to create default attendee
+ if ($this->ui->screen == 'calendar') {
+ $identity = null;
+ foreach ($this->rc->user->list_emails() as $rec) {
+ if (!$identity)
+ $identity = $rec;
+ $identity['emails'][] = $rec['email'];
+ $settings['identities'][$rec['identity_id']] = $rec['email'];
+ }
+ $identity['emails'][] = $this->rc->user->get_username();
+ $settings['identity'] = array('name' => $identity['name'], 'email' => strtolower($identity['email']), 'emails' => ';' . strtolower(join(';', $identity['emails'])));
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Encode events as JSON
+ *
+ * @param array Events as array
+ * @param boolean Add CSS class names according to calendar and categories
+ * @return string JSON encoded events
+ */
+ function encode($events, $addcss = false)
+ {
+ $json = array();
+ foreach ($events as $event) {
+ $json[] = $this->_client_event($event, $addcss);
+ }
+ return json_encode($json);
+ }
+
+ /**
+ * Convert an event object to be used on the client
+ */
+ private function _client_event($event, $addcss = false)
+ {
+ // compose a human readable strings for alarms_text and recurrence_text
+ if ($event['valarms']) {
+ $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']);
+ $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']);
+ }
+ if ($event['recurrence']) {
+ $event['recurrence_text'] = $this->lib->recurrence_text($event['recurrence']);
+ $event['recurrence'] = $this->lib->to_client_recurrence($event['recurrence'], $event['allday']);
+ unset($event['recurrence_date']);
+ }
+
+ foreach ((array)$event['attachments'] as $k => $attachment) {
+ $event['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
+ }
+
+ // Get driver for event calendar
+ $driver = $this->get_driver_by_cal($event['calendar']);
+
+ // convert link URIs references into structs
+ if (array_key_exists('links', $event)) {
+ foreach ((array)$event['links'] as $i => $link) {
+ if (strpos($link, 'imap://') === 0 && ($msgref = $driver->get_message_reference($link))) {
+ $event['links'][$i] = $msgref;
+ }
+ }
+ }
+
+ // check for organizer in attendees list
+ $organizer = null;
+ foreach ((array)$event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
+ $event['attendees'][$i]['noreply'] = true;
+ }
+ else {
+ unset($event['attendees'][$i]['noreply']);
+ }
+ }
+
+ if ($organizer === null && !empty($event['organizer'])) {
+ $organizer = $event['organizer'];
+ $organizer['role'] = 'ORGANIZER';
+ if (!is_array($event['attendees']))
+ $event['attendees'] = array();
+ array_unshift($event['attendees'], $organizer);
+ }
+
+ // mapping url => vurl because of the fullcalendar client script
+ $event['vurl'] = $event['url'];
+ unset($event['url']);
+
+ return array(
+ '_id' => $event['calendar'] . ':' . $event['id'], // unique identifier for fullcalendar
+ 'start' => $this->lib->adjust_timezone($event['start'], $event['allday'])->format('c'),
+ 'end' => $this->lib->adjust_timezone($event['end'], $event['allday'])->format('c'),
+ // 'changed' might be empty for event recurrences (Bug #2185)
+ 'changed' => $event['changed'] ? $this->lib->adjust_timezone($event['changed'])->format('c') : null,
+ 'created' => $event['created'] ? $this->lib->adjust_timezone($event['created'])->format('c') : null,
+ 'title' => strval($event['title']),
+ 'description' => strval($event['description']),
+ 'location' => strval($event['location']),
+ 'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') .
+ 'fc-event-cat-' . asciiwords(strtolower(join('-', (array)$event['categories'])), true) .
+ rtrim(' ' . $event['className']),
+ 'allDay' => ($event['allday'] == 1),
+ ) + $event;
+ }
+
+
+ /**
+ * Generate a unique identifier for an event
+ */
+ public function generate_uid()
+ {
+ return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
+ }
+
+
+ /**
+ * TEMPORARY: generate random event data for testing
+ * Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_driver=kolab&_num=500&_date=2014-08-01&_dev=120
+ */
+ public function generate_randomdata()
+ {
+ @set_time_limit(0);
+
+ $driver = $this->get_driver_by_gpc();
+ $num = $_REQUEST['_num'] ? intval($_REQUEST['_num']) : 100;
+ $date = $_REQUEST['_date'] ?: 'now';
+ $dev = $_REQUEST['_dev'] ?: 30;
+ $cats = array_keys($driver->list_categories());
+ $cals = $driver->list_calendars(calendar_driver::FILTER_ACTIVE);
+ $count = 0;
+
+ while ($count++ < $num) {
+ $spread = intval($dev) * 86400; // days
+ $refdate = strtotime($date);
+ $start = round(($refdate + rand(-$spread, $spread)) / 600) * 600;
+ $duration = round(rand(30, 360) / 30) * 30 * 60;
+ $allday = rand(0,20) > 18;
+ $alarm = rand(-30,12) * 5;
+ $fb = rand(0,2);
+
+ if (date('G', $start) > 23)
+ $start -= 3600;
+
+ if ($allday) {
+ $start = strtotime(date('Y-m-d 00:00:00', $start));
+ $duration = 86399;
+ }
+
+ $title = '';
+ $len = rand(2, 12);
+ $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a feature(s) is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform (hereafter referred to without the classical prefix ) retains many applications, as most manufac- tured parts (and many anatomical parts investigated in medical imagery) contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
+// $chars = "!# abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890";
+ for ($i = 0; $i < $len; $i++)
+ $title .= $words[rand(0,count($words)-1)] . " ";
+
+ $driver->new_event(array(
+ 'uid' => $this->generate_uid(),
+ 'start' => new DateTime('@'.$start),
+ 'end' => new DateTime('@'.($start + $duration)),
+ 'allday' => $allday,
+ 'title' => rtrim($title),
+ 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
+ 'categories' => $cats[array_rand($cats)],
+ 'calendar' => array_rand($cals),
+ 'alarms' => $alarm > 0 ? "-{$alarm}M:DISPLAY" : '',
+ 'priority' => rand(0,9),
+ ));
+ }
+
+ $this->rc->output->redirect('');
+ }
+
+ /**
+ * Handler for attachments upload
+ */
+ public function attachment_upload()
+ {
+ $this->lib->attachment_upload(self::SESSION_KEY, 'cal-');
+ }
+
+ /**
+ * Handler for attachments download/displaying
+ */
+ public function attachment_get()
+ {
+ // show loading page
+ if (!empty($_GET['_preload'])) {
+ return $this->lib->attachment_loading_page();
+ }
+
+ $event_id = rcube_utils::get_input_value('_event', rcube_utils::INPUT_GPC);
+ $calendar = rcube_utils::get_input_value('_cal', rcube_utils::INPUT_GPC);
+ $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
+ $driver = $this->get_driver_by_cal($calendar);
+
+ $event = array('id' => $event_id, 'calendar' => $calendar, 'rev' => $rev);
+ $attachment = $driver->get_attachment($id, $event);
+
+ // show part page
+ if (!empty($_GET['_frame'])) {
+ $this->lib->attachment = $attachment;
+ $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
+ $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
+ $this->rc->output->send('calendar.attachment');
+ }
+ // deliver attachment content
+ else if ($attachment) {
+ $attachment['body'] = $driver->get_attachment_body($id, $event);
+ $this->lib->attachment_get($attachment);
+ }
+
+ // if we arrive here, the requested part was not found
+ header('HTTP/1.1 404 Not Found');
+ exit;
+ }
+
+
+ /**
+ * Prepares new/edited event properties before save
+ */
+ private function write_preprocess(&$event, $action)
+ {
+ // convert dates into DateTime objects in user's current timezone
+ $event['start'] = new DateTime($event['start'], $this->timezone);
+ $event['end'] = new DateTime($event['end'], $this->timezone);
+ $event['allday'] = (bool)$event['allday'];
+
+ // start/end is all we need for 'move' action (#1480)
+ if ($action == 'move') {
+ return;
+ }
+
+ // convert the submitted recurrence settings
+ if (is_array($event['recurrence'])) {
+ $event['recurrence'] = $this->lib->from_client_recurrence($event['recurrence'], $event['start']);
+ }
+
+ // convert the submitted alarm values
+ if ($event['valarms']) {
+ $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']);
+ }
+
+ $attachments = array();
+ $eventid = 'cal-'.$event['id'];
+
+ if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
+ if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
+ foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
+ if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
+ $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
+ }
+ }
+ }
+ }
+
+ $event['attachments'] = $attachments;
+
+ // convert link references into simple URIs
+ if (array_key_exists('links', $event)) {
+ $event['links'] = array_map(function($link) {
+ return is_array($link) ? $link['uri'] : strval($link);
+ }, (array)$event['links']);
+ }
+
+ // check for organizer in attendees
+ if ($action == 'new' || $action == 'edit') {
+ if (!$event['attendees'])
+ $event['attendees'] = array();
+
+ $emails = $this->get_user_emails();
+ $organizer = $owner = false;
+ foreach ((array)$event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER')
+ $organizer = $i;
+ if ($attendee['email'] == in_array(strtolower($attendee['email']), $emails))
+ $owner = $i;
+ if (!isset($attendee['rsvp']))
+ $event['attendees'][$i]['rsvp'] = true;
+ else if (is_string($attendee['rsvp']))
+ $event['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
+ }
+
+ // set new organizer identity
+ if ($organizer !== false && !empty($event['_identity']) && ($identity = $this->rc->user->get_identity($event['_identity']))) {
+ $event['attendees'][$organizer]['name'] = $identity['name'];
+ $event['attendees'][$organizer]['email'] = $identity['email'];
+ }
+
+ // set owner as organizer if yet missing
+ if ($organizer === false && $owner !== false) {
+ $event['attendees'][$owner]['role'] = 'ORGANIZER';
+ unset($event['attendees'][$owner]['rsvp']);
+ }
+ }
+
+ // mapping url => vurl because of the fullcalendar client script
+ if (array_key_exists('vurl', $event)) {
+ $event['url'] = $event['vurl'];
+ unset($event['vurl']);
+ }
+ }
+
+ /**
+ * Releases some resources after successful event save
+ */
+ private function cleanup_event(&$event)
+ {
+ // remove temp. attachment files
+ if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
+ $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
+ $this->rc->session->remove(self::SESSION_KEY);
+ }
+ }
+
+ /**
+ * Send out an invitation/notification to all event attendees
+ */
+ private function notify_attendees($event, $old, $action = 'edit', $comment = null)
+ {
+ if ($action == 'remove' || ($event['status'] == 'CANCELLED' && $old['status'] != $event['status'])) {
+ $event['cancelled'] = true;
+ $is_cancelled = true;
+ }
+
+ $itip = $this->load_itip();
+ $emails = $this->get_user_emails();
+ $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+
+ // add comment to the iTip attachment
+ $event['comment'] = $comment;
+
+ // set a valid recurrence-id if this is a recurrence instance
+ libcalendaring::identify_recurrence_instance($event);
+
+ // compose multipart message using PEAR:Mail_Mime
+ $method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
+ $message = $itip->compose_itip_message($event, $method, !$old || $event['sequence'] > $old['sequence']);
+
+ // list existing attendees from $old event
+ $old_attendees = array();
+ foreach ((array)$old['attendees'] as $attendee) {
+ $old_attendees[] = $attendee['email'];
+ }
+
+ // send to every attendee
+ $sent = 0; $current = array();
+ foreach ((array)$event['attendees'] as $attendee) {
+ $current[] = strtolower($attendee['email']);
+
+ // skip myself for obvious reasons
+ if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails))
+ continue;
+
+ // skip if notification is disabled for this attendee
+ if ($attendee['noreply'] && $itip_notify & 2)
+ continue;
+
+ // skip if this attendee has delegated and set RSVP=FALSE
+ if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false)
+ continue;
+
+ // which template to use for mail text
+ $is_new = !in_array($attendee['email'], $old_attendees);
+ $is_rsvp = $is_new || $event['sequence'] > $old['sequence'];
+ $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
+ $subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
+
+ $event['comment'] = $comment;
+
+ // finally send the message
+ if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
+ $sent++;
+ else
+ $sent = -100;
+ }
+
+ // TODO: on change of a recurring (main) event, also send updates to differing attendess of recurrence exceptions
+
+ // send CANCEL message to removed attendees
+ foreach ((array)$old['attendees'] as $attendee) {
+ if ($attendee['role'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current))
+ continue;
+
+ $vevent = $old;
+ $vevent['cancelled'] = $is_cancelled;
+ $vevent['attendees'] = array($attendee);
+ $vevent['comment'] = $comment;
+ if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
+ $sent++;
+ else
+ $sent = -100;
+ }
+
+ return $sent;
+ }
+
+ private function _get_freebusy_list($email, $start, $end)
+ {
+ $fblist = array();
+ foreach($this->get_drivers() as $driver){
+ if($driver->freebusy) {
+ $cur = $driver->get_freebusy_list($email, $start, $end);
+ if($cur) {
+ $fblist = array_merge($fblist, $cur);
+ }
+ }
+ }
+
+ if(sizeof($fblist) == 0) return false;
+ else return $fblist;
+ }
+
+ /**
+ * Echo simple free/busy status text for the given user and time range
+ */
+ public function freebusy_status()
+ {
+ $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
+ $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
+ $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
+
+ // convert dates into unix timestamps
+ if (!empty($start) && !is_numeric($start)) {
+ $dts = new DateTime($start, $this->timezone);
+ $start = $dts->format('U');
+ }
+ if (!empty($end) && !is_numeric($end)) {
+ $dte = new DateTime($end, $this->timezone);
+ $end = $dte->format('U');
+ }
+
+ if (!$start) $start = time();
+ if (!$end) $end = $start + 3600;
+
+ $fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
+ $status = 'UNKNOWN';
+
+ // if the backend has free-busy information
+ $fblist = $this->_get_freebusy_list($email, $start, $end);
+ if (is_array($fblist)) {
+ $status = 'FREE';
+
+ foreach ($fblist as $slot) {
+ list($from, $to, $type) = $slot;
+ if ($from < $end && $to > $start) {
+ $status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
+ break;
+ }
+ }
+ }
+
+ // let this information be cached for 5min
+ send_future_expire_header(300);
+
+ echo $status;
+ exit;
+ }
+
+ /**
+ * Return a list of free/busy time slots within the given period
+ * Echo data in JSON encoding
+ */
+ public function freebusy_times()
+ {
+ $email = rcube_utils::get_input_value('email', rcube_utils::INPUT_GPC);
+ $start = rcube_utils::get_input_value('start', rcube_utils::INPUT_GPC);
+ $end = rcube_utils::get_input_value('end', rcube_utils::INPUT_GPC);
+ $interval = intval(rcube_utils::get_input_value('interval', rcube_utils::INPUT_GPC));
+ $strformat = $interval > 60 ? 'Ymd' : 'YmdHis';
+
+ // convert dates into unix timestamps
+ if (!empty($start) && !is_numeric($start)) {
+ $dts = rcube_utils::anytodatetime($start, $this->timezone);
+ $start = $dts ? $dts->format('U') : null;
+ }
+ if (!empty($end) && !is_numeric($end)) {
+ $dte = rcube_utils::anytodatetime($end, $this->timezone);
+ $end = $dte ? $dte->format('U') : null;
+ }
+
+ if (!$start) $start = time();
+ if (!$end) $end = $start + 86400 * 30;
+ if (!$interval) $interval = 60; // 1 hour
+
+ if (!$dte) {
+ $dts = new DateTime('@'.$start);
+ $dts->setTimezone($this->timezone);
+ }
+
+ $fblist = $this->_get_freebusy_list($email, $start, $end);
+ $slots = array();
+
+ // build a list from $start till $end with blocks representing the fb-status
+ for ($s = 0, $t = $start; $t <= $end; $s++) {
+ $status = self::FREEBUSY_UNKNOWN;
+ $t_end = $t + $interval * 60;
+ $dt = new DateTime('@'.$t);
+ $dt->setTimezone($this->timezone);
+
+ // determine attendee's status
+ if (is_array($fblist)) {
+ $status = self::FREEBUSY_FREE;
+ foreach ($fblist as $slot) {
+ list($from, $to, $type) = $slot;
+
+ // check for possible all-day times
+ if (gmdate('His', $from) == '000000' && gmdate('His', $to) == '235959') {
+ // shift into the user's timezone for sane matching
+ $from -= $this->gmt_offset;
+ $to -= $this->gmt_offset;
+ }
+
+ if ($from < $t_end && $to > $t) {
+ $status = isset($type) ? $type : self::FREEBUSY_BUSY;
+ if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
+ break;
+ }
+ }
+ }
+
+ $slots[$s] = $status;
+ $times[$s] = intval($dt->format($strformat));
+ $t = $t_end;
+ }
+
+ $dte = new DateTime('@'.$t_end);
+ $dte->setTimezone($this->timezone);
+
+ // let this information be cached for 5min
+ send_future_expire_header(300);
+
+ echo json_encode(array(
+ 'email' => $email,
+ 'start' => $dts->format('c'),
+ 'end' => $dte->format('c'),
+ 'interval' => $interval,
+ 'slots' => $slots,
+ 'times' => $times,
+ ));
+ exit;
+ }
+
+ /**
+ * Handler for printing calendars
+ */
+ public function print_view()
+ {
+ $title = $this->gettext('print');
+
+ $view = rcube_utils::get_input_value('view', rcube_utils::INPUT_GPC);
+ if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
+ $view = 'agendaDay';
+
+ $this->rc->output->set_env('view',$view);
+
+ if ($date = rcube_utils::get_input_value('date', rcube_utils::INPUT_GPC))
+ $this->rc->output->set_env('date', $date);
+
+ if ($range = rcube_utils::get_input_value('range', rcube_utils::INPUT_GPC))
+ $this->rc->output->set_env('listRange', intval($range));
+
+ if (isset($_REQUEST['sections']))
+ $this->rc->output->set_env('listSections', rcube_utils::get_input_value('sections', rcube_utils::INPUT_GPC));
+
+ if ($search = rcube_utils::get_input_value('search', rcube_utils::INPUT_GPC)) {
+ $this->rc->output->set_env('search', $search);
+ $title .= ' "' . $search . '"';
+ }
+
+ // Add CSS stylesheets to the page header
+ $skin_path = $this->local_skin_path();
+ $this->include_stylesheet($skin_path . '/fullcalendar.css');
+ $this->include_stylesheet($skin_path . '/print.css');
+
+ // Add JS files to the page header
+ $this->include_script('print.js');
+ $this->include_script('lib/js/fullcalendar.js');
+
+ $this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
+ $this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
+
+ $this->rc->output->set_pagetitle($title);
+ $this->rc->output->send("calendar.print");
+ }
+
+ /**
+ *
+ */
+ public function get_inline_ui()
+ {
+ foreach (array('save','cancel','savingdata') as $label)
+ $texts['calendar.'.$label] = $this->gettext($label);
+
+ $texts['calendar.new_event'] = $this->gettext('createfrommail');
+
+ $this->ui->init_templates();
+ $this->ui->calendar_list(); # set env['calendars']
+ echo $this->api->output->parse('calendar.eventedit', false, false);
+ echo html::tag('script', array('type' => 'text/javascript'),
+ "rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n".
+ "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
+ "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
+ "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
+ "rcmail.gui_object('attachmentlist', '" . $this->ui->attachmentlist_id . "');\n".
+ "rcmail.add_label(" . json_encode($texts) . ");\n"
+ );
+ exit;
+ }
+
+ /**
+ * Compare two event objects and return differing properties
+ *
+ * @param array Event A
+ * @param array Event B
+ * @return array List of differing event properties
+ */
+ public static function event_diff($a, $b)
+ {
+ $diff = array();
+ $ignore = array('changed' => 1, 'attachments' => 1);
+ foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
+ if (!$ignore[$key] && $key[0] != '_' && $a[$key] != $b[$key])
+ $diff[] = $key;
+ }
+
+ // only compare number of attachments
+ if (count($a['attachments']) != count($b['attachments']))
+ $diff[] = 'attachments';
+
+ return $diff;
+ }
+
+ /**
+ * Update attendee properties on the given event object
+ *
+ * @param array The event object to be altered
+ * @param array List of hash arrays each represeting an updated/added attendee
+ */
+ public static function merge_attendee_data(&$event, $attendees, $removed = null)
+ {
+ if (!empty($attendees) && !is_array($attendees[0])) {
+ $attendees = array($attendees);
+ }
+
+ foreach ($attendees as $attendee) {
+ $found = false;
+
+ foreach ($event['attendees'] as $i => $candidate) {
+ if ($candidate['email'] == $attendee['email']) {
+ $event['attendees'][$i] = $attendee;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $event['attendees'][] = $attendee;
+ }
+ }
+
+ // filter out removed attendees
+ if (!empty($removed)) {
+ $event['attendees'] = array_filter($event['attendees'], function($attendee) use ($removed) {
+ return !in_array($attendee['email'], $removed);
+ });
+ }
+ }
+
+
+ /**** Resource management functions ****/
+
+ /**
+ * Getter for the configured implementation of the resource directory interface
+ */
+ private function resources_directory()
+ {
+ if (is_object($this->resources_dir)) {
+ return $this->resources_dir;
+ }
+
+ if ($driver_name = $this->rc->config->get('calendar_resources_driver')) {
+ $driver_class = 'resources_driver_' . $driver_name;
+
+ require_once($this->home . '/drivers/resources_driver.php');
+ require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+
+ $this->resources_dir = new $driver_class($this);
+ }
+
+ return $this->resources_dir;
+ }
+
+ /**
+ * Handler for resoruce autocompletion requests
+ */
+ public function resources_autocomplete()
+ {
+ $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
+ $sid = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
+ $maxnum = (int)$this->rc->config->get('autocomplete_max', 15);
+ $results = array();
+
+ if ($directory = $this->resources_directory()) {
+ foreach ($directory->load_resources($search, $maxnum) as $rec) {
+ $results[] = array(
+ 'name' => $rec['name'],
+ 'email' => $rec['email'],
+ 'type' => $rec['_type'],
+ );
+ }
+ }
+
+ $this->rc->output->command('ksearch_query_results', $results, $search, $sid);
+ $this->rc->output->send();
+ }
+
+ /**
+ * Handler for load-requests for resource data
+ */
+ function resources_list()
+ {
+ $data = array();
+
+ if ($directory = $this->resources_directory()) {
+ foreach ($directory->load_resources() as $rec) {
+ $data[] = $rec;
+ }
+ }
+
+ $this->rc->output->command('plugin.resource_data', $data);
+ $this->rc->output->send();
+ }
+
+ /**
+ * Handler for requests loading resource owner information
+ */
+ function resources_owner()
+ {
+ if ($directory = $this->resources_directory()) {
+ $id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ $data = $directory->get_resource_owner($id);
+ }
+
+ $this->rc->output->command('plugin.resource_owner', $data);
+ $this->rc->output->send();
+ }
+
+ /**
+ * Deliver event data for a resource's calendar
+ */
+ function resources_calendar()
+ {
+ $events = array();
+
+ if ($directory = $this->resources_directory()) {
+ $events = $directory->get_resource_calendar(
+ rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
+ rcube_utils::get_input_value('start', rcube_utils::INPUT_GET),
+ rcube_utils::get_input_value('end', rcube_utils::INPUT_GET));
+ }
+
+ echo $this->encode($events);
+ exit;
+ }
+
+
+ /**** Event invitation plugin hooks ****/
+
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ function event_itip_status()
+ {
+ $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+
+ // find local copy of the referenced event
+ $driver = null;
+
+ if(isset($data["uid"]))
+ $driver = $this->get_driver_by_event($data["uid"]);
+
+ if($driver == null)
+ $driver = $this->get_default_driver();
+
+ $existing = $driver->get_event($data, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL);
+
+ $itip = $this->load_itip();
+ $response = $itip->get_itip_status($data, $existing);
+
+ // get a list of writeable calendars to save new events to
+ if (!$existing && !$data['nosave'] && $response['action'] == 'rsvp' || $response['action'] == 'import') {
+ $calendars = $driver->list_calendars(calendar_driver::FILTER_PERSONAL);
+ $calendar_select = new html_select(array('name' => 'calendar', 'id' => 'itip-saveto', 'is_escaped' => true));
+ $calendar_select->add('--', '');
+ $numcals = 0;
+ foreach ($calendars as $calendar) {
+ if ($calendar['editable']) {
+ $calendar_select->add($calendar['name'], $calendar['id']);
+ $numcals++;
+ }
+ }
+ if ($numcals <= 1)
+ $calendar_select = null;
+ }
+
+ if ($calendar_select) {
+ $default_calendar = $this->get_default_calendar($data['sensitivity']);
+ $response['select'] = html::span('folder-select', $this->gettext('saveincalendar') . '&nbsp;' .
+ $calendar_select->show($default_calendar['id']));
+ }
+ else if ($data['nosave']) {
+ $response['select'] = html::tag('input', array('type' => 'hidden', 'name' => 'calendar', 'id' => 'itip-saveto', 'value' => ''));
+ }
+
+ // render small agenda view for the respective day
+ if ($data['method'] == 'REQUEST' && !empty($data['date']) && $response['action'] == 'rsvp') {
+ $event_start = rcube_utils::anytodatetime($data['date']);
+ $day_start = new Datetime(gmdate('Y-m-d 00:00', $data['date']), $this->lib->timezone);
+ $day_end = new Datetime(gmdate('Y-m-d 23:59', $data['date']), $this->lib->timezone);
+
+ // get events on that day from the user's personal calendars
+ $calendars = $driver->list_calendars(calendar_driver::FILTER_PERSONAL);
+ $events = $driver->load_events($day_start->format('U'), $day_end->format('U'), null, array_keys($calendars));
+ usort($events, function($a, $b) { return $a['start'] > $b['start'] ? 1 : -1; });
+
+ $before = $after = array();
+ foreach ($events as $event) {
+ // TODO: skip events with free_busy == 'free' ?
+ if ($event['uid'] == $data['uid'] || $event['end'] < $day_start || $event['start'] > $day_end)
+ continue;
+ else if ($event['start'] < $event_start)
+ $before[] = $this->mail_agenda_event_row($event);
+ else
+ $after[] = $this->mail_agenda_event_row($event);
+ }
+
+ $response['append'] = array(
+ 'selector' => '.calendar-agenda-preview',
+ 'replacements' => array(
+ '%before%' => !empty($before) ? join("\n", array_slice($before, -3)) : html::div('event-row no-event', $this->gettext('noearlierevents')),
+ '%after%' => !empty($after) ? join("\n", array_slice($after, 0, 3)) : html::div('event-row no-event', $this->gettext('nolaterevents')),
+ ),
+ );
+ }
+
+ $this->rc->output->command('plugin.update_itip_object_status', $response);
+ }
+
+ /**
+ * Handler for calendar/itip-remove requests
+ */
+ function event_itip_remove()
+ {
+ $success = false;
+ $uid = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
+
+ // search for event if only UID is given
+ $driver = $this->get_driver_by_event($uid);
+ if ($event = $driver->get_event(array('uid' => $uid, '_instance' => $instance), calendar_driver::FILTER_WRITEABLE)) {
+ $event['_savemode'] = $savemode;
+ $success = $driver->remove_event($event, true);
+ }
+
+ if ($success) {
+ $this->rc->output->show_message('calendar.successremoval', 'confirmation');
+ }
+ else {
+ $this->rc->output->show_message('calendar.errorsaving', 'error');
+ }
+ }
+
+ /**
+ * Handler for URLs that allow an invitee to respond on his invitation mail
+ */
+ public function itip_attend_response($p)
+ {
+ if ($p['action'] == 'attend') {
+ $this->ui->init();
+
+ $this->rc->output->set_env('task', 'calendar'); // override some env vars
+ $this->rc->output->set_env('refresh_interval', 0);
+ $this->rc->output->set_pagetitle($this->gettext('calendar'));
+
+ $itip = $this->load_itip();
+ $token = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
+
+ // read event info stored under the given token
+ if ($invitation = $itip->get_invitation($token)) {
+ $this->token = $token;
+ $this->event = $invitation['event'];
+
+ // show message about cancellation
+ if ($invitation['cancelled']) {
+ $this->invitestatus = html::div('rsvp-status declined', $itip->gettext('eventcancelled'));
+ }
+ // save submitted RSVP status
+ else if (!empty($_POST['rsvp'])) {
+ $status = null;
+ foreach (array('accepted','tentative','declined') as $method) {
+ if ($_POST['rsvp'] == $itip->gettext('itip' . $method)) {
+ $status = $method;
+ break;
+ }
+ }
+
+ // send itip reply to organizer
+ $invitation['event']['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+ if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
+ $this->invitestatus = html::div('rsvp-status ' . strtolower($status), $itip->gettext('youhave'.strtolower($status)));
+ }
+ else
+ $this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
+
+ // if user is logged in...
+ if ($this->rc->user->ID) {
+ $invitation = $itip->get_invitation($token);
+ $driver = $this->get_driver_by_cal($invitation['event']['calendar']);
+
+ // save the event to his/her default calendar if not yet present
+ if (!$driver->get_event($this->event) && ($calendar = $this->get_default_calendar($invitation['event']['sensitivity']))) {
+ $invitation['event']['calendar'] = $calendar['id'];
+ if ($driver->new_event($invitation['event']))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
+ }
+ }
+ }
+
+ $this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
+ $this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
+
+ if (!$this->invitestatus) {
+ $this->itip->set_rsvp_actions(array('accepted','tentative','declined'));
+ $this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
+ }
+
+ $this->rc->output->set_pagetitle($itip->gettext('itipinvitation') . ' ' . $this->event['title']);
+ }
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
+
+ $this->rc->output->send('calendar.itipattend');
+ }
+ }
+
+ /**
+ *
+ */
+ public function itip_event_inviteform($attrib)
+ {
+ $hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
+ return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
+ }
+
+ /**
+ *
+ */
+ private function mail_agenda_event_row($event, $class = '')
+ {
+ $time = $event['allday'] ? $this->gettext('all-day') :
+ $this->rc->format_date($event['start'], $this->rc->config->get('time_format')) . ' - ' .
+ $this->rc->format_date($event['end'], $this->rc->config->get('time_format'));
+
+ return html::div(rtrim('event-row ' . $class),
+ html::span('event-date', $time) .
+ html::span('event-title', Q($event['title']))
+ );
+ }
+
+ /**
+ *
+ */
+ public function mail_messages_list($p)
+ {
+ if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) {
+ foreach ($p['messages'] as $header) {
+ $part = new StdClass;
+ $part->mimetype = $header->ctype;
+ if (libcalendaring::part_is_vcalendar($part)) {
+ $header->list_flags['attachmentClass'] = 'ical';
+ }
+ else if (in_array($header->ctype, array('multipart/alternative', 'multipart/mixed'))) {
+ // TODO: fetch bodystructure and search for ical parts. Maybe too expensive?
+
+ if (!empty($header->structure) && is_array($header->structure->parts)) {
+ foreach ($header->structure->parts as $part) {
+ if (libcalendaring::part_is_vcalendar($part) && !empty($part->ctype_parameters['method'])) {
+ $header->list_flags['attachmentClass'] = 'ical';
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Add UI element to copy event invitations or updates to the calendar
+ */
+ public function mail_messagebody_html($p)
+ {
+ // load iCalendar functions (if necessary)
+ if (!empty($this->lib->ical_parts)) {
+ $this->get_ical();
+ $this->load_itip();
+ }
+
+ $html = '';
+ $has_events = false;
+ $ical_objects = $this->lib->get_mail_ical_objects();
+
+ // show a box for every event in the file
+ foreach ($ical_objects as $idx => $event) {
+ if ($event['_type'] != 'event') // skip non-event objects (#2928)
+ continue;
+
+ $has_events = true;
+
+ // get prepared inline UI for this event object
+ if ($ical_objects->method) {
+ $append = '';
+
+ // prepare a small agenda preview to be filled with actual event data on async request
+ if ($ical_objects->method == 'REQUEST') {
+ $append = html::div('calendar-agenda-preview',
+ html::tag('h3', 'preview-title', $this->gettext('agenda') . ' ' .
+ html::span('date', $this->rc->format_date($event['start'], $this->rc->config->get('date_format')))
+ ) . '%before%' . $this->mail_agenda_event_row($event, 'current') . '%after%');
+ }
+
+ $html .= html::div('calendar-invitebox',
+ $this->itip->mail_itip_inline_ui(
+ $event,
+ $ical_objects->method,
+ $ical_objects->mime_id . ':' . $idx,
+ 'calendar',
+ rcube_utils::anytodatetime($ical_objects->message_date),
+ $this->rc->url(array('task' => 'calendar')) . '&view=agendaDay&date=' . $event['start']->format('U')
+ ) . $append
+ );
+ }
+
+ // limit listing
+ if ($idx >= 3)
+ break;
+ }
+
+ // prepend event boxes to message body
+ if ($html) {
+ $this->ui->init();
+ $p['content'] = $html . $p['content'];
+ $this->rc->output->add_label('calendar.savingdata','calendar.deleteventconfirm','calendar.declinedeleteconfirm');
+ }
+
+ // add "Save to calendar" button into attachment menu
+ if ($has_events) {
+ $this->add_button(array(
+ 'id' => 'attachmentsavecal',
+ 'name' => 'attachmentsavecal',
+ 'type' => 'link',
+ 'wrapper' => 'li',
+ 'command' => 'attachment-save-calendar',
+ 'class' => 'icon calendarlink',
+ 'classact' => 'icon calendarlink active',
+ 'innerclass' => 'icon calendar',
+ 'label' => 'calendar.savetocalendar',
+ ), 'attachmentmenu');
+ }
+
+ return $p;
+ }
+
+
+ /**
+ * Handler for POST request to import an event attached to a mail message
+ */
+ public function mail_import_itip()
+ {
+ $itip_sending = $this->rc->config->get('calendar_itip_send_option', $this->defaults['calendar_itip_send_option']);
+
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
+ $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
+ $status = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
+ $delete = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
+ $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST));
+ $noreply = $noreply || $status == 'needs-action' || $itip_sending === 0;
+ $instance = rcube_utils::get_input_value('_instance', rcube_utils::INPUT_POST);
+ $savemode = rcube_utils::get_input_value('_savemode', rcube_utils::INPUT_POST);
+
+ $error_msg = $this->gettext('errorimportingevent');
+ $success = false;
+ $delegate = null;
+
+ if ($status == 'delegated') {
+ $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
+ $delegate = reset($delegates);
+
+ if (empty($delegate) || empty($delegate['mailto'])) {
+ $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
+ return;
+ }
+ }
+
+ // successfully parsed events?
+ if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
+ // forward iTip request to delegatee
+ if ($delegate) {
+ $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST));
+
+ $itip = $this->load_itip();
+ if ($itip->delegate_to($event, $delegate, $rsvpme ? true : false)) {
+ $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+
+ // the delegator is set to non-participant, thus save as non-blocking
+ $event['free_busy'] = 'free';
+ }
+
+ // find writeable calendar to store event
+ $cal_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
+
+ $calendar = null;
+ $driver = null;
+
+ if($cal_id) {
+ $driver = $this->get_driver_by_cal($cal_id);
+ $calendars = $driver->list_calendars(false, true);
+ $calendar = $calendars[$cal_id];
+ }
+
+ $dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST');
+
+ // select default calendar except user explicitly selected 'none'
+ if (!$calendar && !$dontsave)
+ $calendar = $this->get_default_calendar(true, $event['sensitivity'] == 'confidential');
+
+ if(!$driver) {
+ $driver = $this->get_driver_by_cal($calendar["id"]);
+ }
+
+ // select default calendar except user explicitly selected 'none'
+ if (!$calendar && !$dontsave)
+ $calendar = $this->get_default_calendar($event['sensitivity']);
+
+ $metadata = array(
+ 'uid' => $event['uid'],
+ '_instance' => $event['_instance'],
+ 'changed' => is_object($event['changed']) ? $event['changed']->format('U') : 0,
+ 'sequence' => intval($event['sequence']),
+ 'fallback' => strtoupper($status),
+ 'method' => $event['_method'],
+ 'task' => 'calendar',
+ );
+
+ // update my attendee status according to submitted method
+ if (!empty($status)) {
+ $organizer = null;
+ $emails = $this->get_user_emails();
+ foreach ($event['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $event['attendees'][$i]['status'] = strtoupper($status);
+ if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED')))
+ $event['attendees'][$i]['rsvp'] = false; // unset RSVP attribute
+
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
+ $reply_sender = $attendee['email'];
+ $event_attendee = $attendee;
+ }
+ }
+
+ // add attendee with this user's default identity if not listed
+ if (!$reply_sender) {
+ $sender_identity = $this->rc->user->list_emails(true);
+ $event['attendees'][] = array(
+ 'name' => $sender_identity['name'],
+ 'email' => $sender_identity['email'],
+ 'role' => 'OPT-PARTICIPANT',
+ 'status' => strtoupper($status),
+ );
+ $metadata['attendee'] = $sender_identity['email'];
+ }
+ }
+
+ // save to calendar
+ if ($calendar && $calendar['editable']) {
+ // check for existing event with the same UID
+ $existing = $driver->get_event($event, calendar_driver::FILTER_WRITEABLE | calendar_driver::FILTER_PERSONAL);
+
+ if ($existing) {
+ // forward savemode for correct updates of recurring events
+ $existing['_savemode'] = $savemode ?: $event['_savemode'];
+
+ // only update attendee status
+ if ($event['_method'] == 'REPLY') {
+ // try to identify the attendee using the email sender address
+ $existing_attendee = -1;
+ $existing_attendee_emails = array();
+ foreach ($existing['attendees'] as $i => $attendee) {
+ $existing_attendee_emails[] = $attendee['email'];
+ if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
+ $existing_attendee = $i;
+ }
+ }
+ $event_attendee = null;
+ $update_attendees = array();
+ foreach ($event['attendees'] as $attendee) {
+ if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
+ $event_attendee = $attendee;
+ $update_attendees[] = $attendee;
+ $metadata['fallback'] = $attendee['status'];
+ $metadata['attendee'] = $attendee['email'];
+ $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
+ if ($attendee['status'] != 'DELEGATED') {
+ break;
+ }
+ }
+ // also copy delegate attendee
+ else if (!empty($attendee['delegated-from']) &&
+ (stripos($attendee['delegated-from'], $event['_sender']) !== false ||
+ stripos($attendee['delegated-from'], $event['_sender_utf']) !== false)) {
+ $update_attendees[] = $attendee;
+ if (!in_array($attendee['email'], $existing_attendee_emails)) {
+ $existing['attendees'][] = $attendee;
+ }
+ }
+ }
+
+ // if delegatee has declined, set delegator's RSVP=True
+ if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
+ foreach ($existing['attendees'] as $i => $attendee) {
+ if ($attendee['email'] == $event_attendee['delegated-from']) {
+ $existing['attendees'][$i]['rsvp'] = true;
+ break;
+ }
+ }
+ }
+
+ // found matching attendee entry in both existing and new events
+ if ($existing_attendee >= 0 && $event_attendee) {
+ $existing['attendees'][$existing_attendee] = $event_attendee;
+ $success = $driver->update_attendees($existing, $update_attendees);
+ }
+ // update the entire attendees block
+ else if (($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) && $event_attendee) {
+ $existing['attendees'][] = $event_attendee;
+ $success = $driver->update_attendees($existing, $update_attendees);
+ }
+ else {
+ $error_msg = $this->gettext('newerversionexists');
+ }
+ }
+ // delete the event when declined (#1670)
+ else if ($status == 'declined' && $delete) {
+ $deleted = $driver->remove_event($existing, true);
+ $success = true;
+ }
+ // import the (newer) event
+ else if ($event['sequence'] >= $existing['sequence'] || $event['changed'] >= $existing['changed']) {
+ $event['id'] = $existing['id'];
+ $event['calendar'] = $existing['calendar'];
+
+ // preserve my participant status for regular updates
+ if (empty($status)) {
+ $emails = $this->get_user_emails();
+ foreach ($event['attendees'] as $i => $attendee) {
+ if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ foreach ($existing['attendees'] as $j => $_attendee) {
+ if ($attendee['email'] == $_attendee['email']) {
+ $event['attendees'][$i] = $existing['attendees'][$j];
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // set status=CANCELLED on CANCEL messages
+ if ($event['_method'] == 'CANCEL')
+ $event['status'] = 'CANCELLED';
+ // show me as free when declined (#1670)
+ if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT')
+ $event['free_busy'] = 'free';
+
+ $success = $driver->edit_event($event);
+ }
+ else if (!empty($status)) {
+ $existing['attendees'] = $event['attendees'];
+ if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT') // show me as free when declined (#1670)
+ $existing['free_busy'] = 'free';
+ $success = $driver->edit_event($existing);
+ }
+ else
+ $error_msg = $this->gettext('newerversionexists');
+ }
+ else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_calendars'))) {
+ if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT') {
+ $event['free_busy'] = 'free';
+ }
+
+ // if the RSVP reply only refers to a single instance:
+ // store unmodified master event with current instance as exception
+ if (!empty($instance) && !empty($savemode) && $savemode != 'all') {
+ $master = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event');
+ if ($master['recurrence'] && !$master['_instance']) {
+ // compute recurring events until this instance's date
+ if ($recurrence_date = rcube_utils::anytodatetime($instance, $master['start']->getTimezone())) {
+ $recurrence_date->setTime(23,59,59);
+
+ foreach ($driver->get_recurring_events($master, $master['start'], $recurrence_date) as $recurring) {
+ if ($recurring['_instance'] == $instance) {
+ // copy attendees block with my partstat to exception
+ $recurring['attendees'] = $event['attendees'];
+ $master['recurrence']['EXCEPTIONS'][] = $recurring;
+ $event = $recurring; // set reference for iTip reply
+ break;
+ }
+ }
+
+ $master['calendar'] = $event['calendar'] = $calendar['id'];
+ $success = $driver->new_event($master);
+ }
+ else {
+ $master = null;
+ }
+ }
+ else {
+ $master = null;
+ }
+ }
+
+ // save to the selected/default calendar
+ if (!$master) {
+ $event['calendar'] = $calendar['id'];
+ $success = $driver->new_event($event);
+ }
+ }
+ else if ($status == 'declined')
+ $error_msg = null;
+ }
+ else if ($status == 'declined' || $dontsave)
+ $error_msg = null;
+ else
+ $error_msg = $this->gettext('nowritecalendarfound');
+ }
+
+ if ($success) {
+ $message = $event['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
+ $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
+ }
+
+ if ($success || $dontsave) {
+ $metadata['calendar'] = $event['calendar'];
+ $metadata['nosave'] = $dontsave;
+ $metadata['rsvp'] = intval($metadata['rsvp']);
+ $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', $this->defaults['calendar_itip_after_action']);
+ $this->rc->output->command('plugin.itip_message_processed', $metadata);
+ $error_msg = null;
+ }
+ else if ($error_msg) {
+ $this->rc->output->command('display_message', $error_msg, 'error');
+ }
+
+ // send iTip reply
+ if ($event['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
+ $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+ $itip = $this->load_itip();
+ $itip->set_sender_email($reply_sender);
+ if ($itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+
+ $this->rc->output->send();
+ }
+
+
+ /**
+ * Handler for calendar/itip-remove requests
+ */
+ function mail_itip_decline_reply()
+ {
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
+ $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
+
+ if (($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) && $event['_method'] == 'REPLY') {
+ $event['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+
+ foreach ($event['attendees'] as $_attendee) {
+ if ($_attendee['role'] != 'ORGANIZER') {
+ $attendee = $_attendee;
+ break;
+ }
+ }
+
+ $itip = $this->load_itip();
+ if ($itip->send_itip_message($event, 'CANCEL', $attendee, 'itipsubjectcancel', 'itipmailbodycancel'))
+ $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $attendee['name'] ? $attendee['name'] : $attendee['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+ }
+ }
+
+ /**
+ * Handler for calendar/itip-delegate requests
+ */
+ function mail_itip_delegate()
+ {
+ // forward request to mail_import_itip() with the right status
+ $_POST['_status'] = $_REQUEST['_status'] = 'delegated';
+ $this->mail_import_itip();
+ }
+
+ /**
+ * Import the full payload from a mail message attachment
+ */
+ public function mail_import_attachment()
+ {
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
+ $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
+ $charset = RCMAIL_CHARSET;
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
+
+ if ($uid && $mime_id) {
+ $part = $imap->get_message_part($uid, $mime_id);
+ if ($part->ctype_parameters['charset'])
+ $charset = $part->ctype_parameters['charset'];
+// $headers = $imap->get_message_headers($uid);
+
+ if ($part) {
+ $events = $this->get_ical()->import($part, $charset);
+ }
+ }
+
+ $success = $existing = 0;
+ if (!empty($events)) {
+ // find writeable calendar to store event
+ $cal_id = !empty($_REQUEST['_calendar']) ? rcube_utils::get_input_value('_calendar', rcube_utils::INPUT_POST) : null;
+ $driver = null;
+ if($cal_id) $driver = $this->get_driver_by_cal($cal_id);
+ else $driver = $this->get_driver_by_gpc();
+ $calendars = $driver->list_calendars(calendar_driver::FILTER_PERSONAL);
+
+ foreach ($events as $event) {
+ // save to calendar
+ $calendar = $calendars[$cal_id] ?: $this->get_default_calendar($event['sensitivity']);
+ if ($calendar && $calendar['editable'] && $event['_type'] == 'event') {
+ $event['calendar'] = $calendar['id'];
+
+ if (!$driver->get_event($event['uid'], calendar_driver::FILTER_WRITEABLE)) {
+ $success += (bool)$driver->new_event($event);
+ }
+ else {
+ $existing++;
+ }
+ }
+ }
+ }
+
+ if ($success) {
+ $this->rc->output->command('display_message', $this->gettext(array(
+ 'name' => 'importsuccess',
+ 'vars' => array('nr' => $success),
+ )), 'confirmation');
+ }
+ else if ($existing) {
+ $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('errorimportingevent'), 'error');
+ }
+ }
+
+ /**
+ * Read email message and return contents for a new event based on that message
+ */
+ public function mail_message2event()
+ {
+ $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
+ $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
+ $event = array();
+
+ // establish imap connection
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
+ $message = new rcube_message($uid);
+
+ if ($message->headers) {
+ $event['title'] = trim($message->subject);
+ $event['description'] = trim($message->first_text_part());
+
+ $driver = $this->get_default_driver();
+
+ // add a reference to the email message
+ if ($msgref = $driver->get_message_reference($message->headers, $mbox)) {
+ $event['links'] = array($msgref);
+ }
+ // copy mail attachments to event
+ else if ($message->attachments) {
+ $eventid = 'cal-';
+ if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
+ $_SESSION[self::SESSION_KEY] = array();
+ $_SESSION[self::SESSION_KEY]['id'] = $eventid;
+ $_SESSION[self::SESSION_KEY]['attachments'] = array();
+ }
+
+ foreach ((array)$message->attachments as $part) {
+ $attachment = array(
+ 'data' => $imap->get_message_part($uid, $part->mime_id, $part),
+ 'size' => $part->size,
+ 'name' => $part->filename,
+ 'mimetype' => $part->mimetype,
+ 'group' => $eventid,
+ );
+
+ $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
+
+ if ($attachment['status'] && !$attachment['abort']) {
+ $id = $attachment['id'];
+ $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
+
+ // store new attachment in session
+ unset($attachment['status'], $attachment['abort'], $attachment['data']);
+ $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
+
+ $attachment['id'] = 'rcmfile' . $attachment['id']; // add prefix to consider it 'new'
+ $event['attachments'][] = $attachment;
+ }
+ }
+ }
+
+ $this->rc->output->command('plugin.mail2event_dialog', $event);
+ }
+ else {
+ $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
+ }
+
+ $this->rc->output->send();
+ }
+
+ /**
+ * Handler for the 'message_compose' plugin hook. This will check for
+ * a compose parameter 'calendar_event' and create an attachment with the
+ * referenced event in iCal format
+ */
+ public function mail_message_compose($args)
+ {
+ // set the submitted event ID as attachment
+ if (!empty($args['param']['calendar_event'])) {
+ list($cal, $id) = explode(':', $args['param']['calendar_event'], 2);
+ $driver = $this->get_driver_by_cal($cal);
+ if ($event = $driver->get_event(array('id' => $id, 'calendar' => $cal))) {
+ $filename = asciiwords($event['title']);
+ if (empty($filename))
+ $filename = 'event';
+
+ // save ics to a temp file and register as attachment
+ $tmp_path = tempnam($this->rc->config->get('temp_dir'), 'rcmAttmntCal');
+ file_put_contents($tmp_path, $this->get_ical()->export(array($event), '', false, array($driver, 'get_attachment_body')));
+
+ $args['attachments'][] = array('path' => $tmp_path, 'name' => $filename . '.ics', 'mimetype' => 'text/calendar');
+ $args['param']['subject'] = $event['title'];
+ }
+ }
+
+ return $args;
+ }
+
+
+ /**
+ * Get a list of email addresses of the current user (from login and identities)
+ */
+ public function get_user_emails()
+ {
+ return $this->lib->get_user_emails();
+ }
+
+
+ /**
+ * Build an absolute URL with the given parameters
+ */
+ public function get_url($param = array())
+ {
+ $param += array('task' => 'calendar');
+ return $this->rc->url($param, true, true);
+ }
+
+
+ public function ical_feed_hash($source)
+ {
+ return base64_encode($this->rc->user->get_username() . ':' . $source);
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ // delete itipinvitations entries related to this user
+ $db = $this->rc->get_dbh();
+ $table_itipinvitations = $db->table_name('itipinvitations', true);
+ $db->query("DELETE FROM $table_itipinvitations WHERE `user_id` = ?", $args['user']->ID);
+
+ foreach($this->get_drivers() as $driver)
+ if(!$driver->user_delete($args))
+ return false;
+
+ return true;
+ }
+
+ /**
+ * Magic getter for public access to protected members
+ */
+ public function __get($name)
+ {
+ switch ($name) {
+ case 'ical':
+ return $this->get_ical();
+
+ case 'itip':
+ return $this->load_itip();
+
+ case 'driver':
+ $driver = $this->get_driver_by_gpc(true);
+ if(!$driver) $driver = $this->get_default_driver();
+ return $driver;
+ }
+
+ return null;
+ }
+
+}
diff --git a/calendar/calendar_base.js b/calendar/calendar_base.js
new file mode 100644
index 0000000..41ae8e5
--- /dev/null
+++ b/calendar/calendar_base.js
@@ -0,0 +1,139 @@
+/**
+ * Base Javascript class for the Calendar plugin
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2013-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this page.
+ */
+
+// Basic setup for Roundcube calendar client class
+function rcube_calendar(settings)
+{
+ // extend base class
+ rcube_libcalendaring.call(this, settings);
+
+ // member vars
+ this.ui;
+ this.ui_loaded = false;
+ this.selected_attachment = null;
+
+ // private vars
+ var me = this;
+
+ // create new event from current mail message
+ this.create_from_mail = function(uid)
+ {
+ if (uid || (uid = rcmail.get_single_uid())) {
+ // load calendar UI (scripts and edit dialog template)
+ if (!this.ui_loaded) {
+ $.when(
+ $.getScript(rcmail.assets_path('plugins/calendar/calendar_ui.js')),
+ $.getScript(rcmail.assets_path('plugins/calendar/lib/js/fullcalendar.js')),
+ $.get(rcmail.url('calendar/inlineui'), function(html){ $(document.body).append(html); }, 'html')
+ ).then(function() {
+ // disable attendees feature (autocompletion and stuff is not initialized)
+ for (var c in rcmail.env.calendars)
+ rcmail.env.calendars[c].attendees = rcmail.env.calendars[c].resources = false;
+
+ me.ui_loaded = true;
+ me.ui = new rcube_calendar_ui(me.settings);
+ me.create_from_mail(uid); // start over
+ });
+ return;
+ }
+ else {
+ // get message contents for event dialog
+ var lock = rcmail.set_busy(true, 'loading');
+ rcmail.http_post('calendar/mailtoevent', {
+ '_mbox': rcmail.env.mailbox,
+ '_uid': uid
+ }, lock);
+ }
+ }
+ };
+
+ // callback function triggered from server with contents for the new event
+ this.mail2event_dialog = function(event)
+ {
+ if (event.title) {
+ this.ui.add_event(event);
+ if (rcmail.message_list)
+ rcmail.message_list.blur();
+ }
+ };
+
+ // handler for attachment-save-calendar commands
+ this.save_to_calendar = function(p)
+ {
+ // TODO: show dialog to select the calendar for importing
+ if (this.selected_attachment && window.rcube_libcalendaring) {
+ rcmail.http_post('calendar/mailimportattach', {
+ _uid: rcmail.env.uid,
+ _mbox: rcmail.env.mailbox,
+ _part: this.selected_attachment,
+ // _calendar: $('#calendar-attachment-saveto').val(),
+ }, rcmail.set_busy(true, 'itip.savingdata'));
+ }
+ }
+}
+
+
+/* calendar plugin initialization (for non-calendar tasks) */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+ if (rcmail.task != 'calendar') {
+ var cal = new rcube_calendar($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
+
+ // register create-from-mail command to message_commands array
+ if (rcmail.env.task == 'mail') {
+ rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() });
+ rcmail.register_command('attachment-save-calendar', function() { cal.save_to_calendar() });
+ rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) });
+ rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); });
+
+ if (rcmail.env.action != 'show') {
+ rcmail.env.message_commands.push('calendar-create-from-mail');
+ rcmail.add_element($('<a>'));
+ }
+ else {
+ rcmail.enable_command('calendar-create-from-mail', true);
+ }
+
+ rcmail.addEventListener('beforemenu-open', function(p) {
+ if (p.menu == 'attachmentmenu') {
+ cal.selected_attachment = p.id;
+ var mimetype = rcmail.env.attachments[p.id];
+ rcmail.enable_command('attachment-save-calendar', mimetype == 'text/calendar' || mimetype == 'text/x-vcalendar' || mimetype == 'application/ics');
+ }
+ });
+ }
+ }
+
+ rcmail.register_command('plugin.calendar', function() { rcmail.switch_task('calendar'); }, true);
+
+ rcmail.addEventListener('plugin.ping_url', function(p){
+ var action = p.action;
+ p.action = p.event = null;
+ new Image().src = rcmail.url(action, p);
+ });
+});
diff --git a/calendar/calendar_ui.js b/calendar/calendar_ui.js
new file mode 100644
index 0000000..452ea38
--- /dev/null
+++ b/calendar/calendar_ui.js
@@ -0,0 +1,4273 @@
+/**
+ * Client UI Javascript for the Calendar plugin
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+// Roundcube calendar UI client class
+function rcube_calendar_ui(settings)
+{
+ // extend base class
+ rcube_calendar.call(this, settings);
+
+ /*** member vars ***/
+ this.is_loading = false;
+ this.selected_event = null;
+ this.selected_calendar = null;
+ this.search_request = null;
+ this.saving_lock;
+ this.calendars = {};
+ this.quickview_sources = [];
+
+
+ /*** private vars ***/
+ var DAY_MS = 86400000;
+ var HOUR_MS = 3600000;
+ var me = this;
+ var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
+ var client_timezone = new Date().getTimezoneOffset();
+ var day_clicked = day_clicked_ts = 0;
+ var ignore_click = false;
+ var event_defaults = { free_busy:'busy', alarms:'' };
+ var event_attendees = [];
+ var calendars_list;
+ var calenders_search_list;
+ var calenders_search_container;
+ var search_calendars = {};
+ var attendees_list;
+ var resources_list;
+ var resources_treelist;
+ var resources_data = {};
+ var resources_index = [];
+ var resource_owners = {};
+ var resources_events_source = { url:null, editable:false };
+ var freebusy_ui = { workinhoursonly:false, needsupdate:false };
+ var freebusy_data = {};
+ var current_view = null;
+ var count_sources = [];
+ var exec_deferred = bw.ie6 ? 5 : 1;
+ var sensitivitylabels = { 'public':rcmail.gettext('public','calendar'), 'private':rcmail.gettext('private','calendar'), 'confidential':rcmail.gettext('confidential','calendar') };
+ var ui_loading = rcmail.set_busy(true, 'loading');
+
+ // general datepicker settings
+ var datepicker_settings = {
+ // translate from fullcalendar format to datepicker format
+ dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
+ firstDay : settings['first_day'],
+ dayNamesMin: settings['days_short'],
+ monthNames: settings['months'],
+ monthNamesShort: settings['months'],
+ changeMonth: false,
+ showOtherMonths: true,
+ selectOtherMonths: true
+ };
+
+ // global fullcalendar settings
+ var fullcalendar_defaults = {
+ aspectRatio: 1,
+ ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
+ monthNames : settings.months,
+ monthNamesShort : settings.months_short,
+ dayNames : settings.days,
+ dayNamesShort : settings.days_short,
+ firstDay : settings.first_day,
+ firstHour : settings.first_hour,
+ slotMinutes : 60/settings.timeslots,
+ timeFormat: {
+ '': settings.time_format,
+ agenda: settings.time_format + '{ - ' + settings.time_format + '}',
+ list: settings.time_format + '{ - ' + settings.time_format + '}',
+ table: settings.time_format + '{ - ' + settings.time_format + '}'
+ },
+ axisFormat : settings.time_format,
+ columnFormat: {
+ month: 'ddd', // Mon
+ week: 'ddd ' + settings.date_short, // Mon 9/7
+ day: 'dddd ' + settings.date_short, // Monday 9/7
+ table: settings.date_agenda
+ },
+ titleFormat: {
+ month: 'MMMM yyyy',
+ week: settings.dates_long,
+ day: 'dddd ' + settings['date_long'],
+ table: settings.dates_long
+ },
+ listPage: 7, // advance one week in agenda view
+ listRange: settings.agenda_range,
+ listSections: settings.agenda_sections,
+ tableCols: ['handle', 'date', 'time', 'title', 'location'],
+ defaultView: rcmail.env.view || settings.default_view,
+ allDayText: rcmail.gettext('all-day', 'calendar'),
+ buttonText: {
+ prev: '&nbsp;&#9668;&nbsp;',
+ next: '&nbsp;&#9658;&nbsp;',
+ today: settings['today'],
+ day: rcmail.gettext('day', 'calendar'),
+ week: rcmail.gettext('week', 'calendar'),
+ month: rcmail.gettext('month', 'calendar'),
+ table: rcmail.gettext('agenda', 'calendar')
+ },
+ listTexts: {
+ until: rcmail.gettext('until', 'calendar'),
+ past: rcmail.gettext('pastevents', 'calendar'),
+ today: rcmail.gettext('today', 'calendar'),
+ tomorrow: rcmail.gettext('tomorrow', 'calendar'),
+ thisWeek: rcmail.gettext('thisweek', 'calendar'),
+ nextWeek: rcmail.gettext('nextweek', 'calendar'),
+ thisMonth: rcmail.gettext('thismonth', 'calendar'),
+ nextMonth: rcmail.gettext('nextmonth', 'calendar'),
+ future: rcmail.gettext('futureevents', 'calendar'),
+ week: rcmail.gettext('weekofyear', 'calendar')
+ },
+ currentTimeIndicator: settings.time_indicator,
+ // event rendering
+ eventRender: function(event, element, view) {
+ if (view.name != 'list' && view.name != 'table') {
+ var prefix = event.sensitivity && event.sensitivity != 'public' ? String(sensitivitylabels[event.sensitivity]).toUpperCase()+': ' : '';
+ element.attr('title', prefix + event.title);
+ }
+ if (view.name != 'month') {
+ if (event.location) {
+ element.find('div.fc-event-title').after('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
+ }
+ if (event.sensitivity && event.sensitivity != 'public')
+ element.find('div.fc-event-time').append('<i class="fc-icon-sensitive"></i>');
+ if (event.recurrence)
+ element.find('div.fc-event-time').append('<i class="fc-icon-recurring"></i>');
+ if (event.alarms || (event.valarms && event.valarms.length))
+ element.find('div.fc-event-time').append('<i class="fc-icon-alarms"></i>');
+ }
+ if (event.status) {
+ element.addClass('cal-event-status-' + String(event.status).toLowerCase());
+ }
+
+ element.attr('aria-label', event.title + ', ' + me.event_date_text(event, true));
+ },
+ // render element indicating more (invisible) events
+ overflowRender: function(data, element) {
+ element.html(rcmail.gettext('andnmore', 'calendar').replace('$nr', data.count))
+ .click(function(e){ me.fisheye_view(data.date); });
+ },
+ // callback when a specific event is clicked
+ eventClick: function(event, ev, view) {
+ if (!event.temp && String(event.className).indexOf('fc-type-freebusy') < 0)
+ event_show_dialog(event, ev);
+ }
+ };
+
+ /*** imports ***/
+ var Q = this.quote_html;
+ var text2html = this.text2html;
+ var event_date_text = this.event_date_text;
+ var parse_datetime = this.parse_datetime;
+ var date2unixtime = this.date2unixtime;
+ var fromunixtime = this.fromunixtime;
+ var parseISO8601 = this.parseISO8601;
+ var date2servertime = this.date2ISO8601;
+ var render_message_links = this.render_message_links;
+
+
+ /*** private methods ***/
+
+ // same as str.split(delimiter) but it ignores delimiters within quoted strings
+ var explode_quoted_string = function(str, delimiter)
+ {
+ var result = [],
+ strlen = str.length,
+ q, p, i, char, last;
+
+ for (q = p = i = 0; i < strlen; i++) {
+ char = str.charAt(i);
+ if (char == '"' && last != '\\') {
+ q = !q;
+ }
+ else if (!q && char == delimiter) {
+ result.push(str.substring(p, i));
+ p = i + 1;
+ }
+ last = char;
+ }
+
+ result.push(str.substr(p));
+ return result;
+ };
+
+ // Change the first charcter to uppercase
+ var ucfirst = function(str)
+ {
+ return str.charAt(0).toUpperCase() + str.substr(1);
+ };
+
+ // clone the given date object and optionally adjust time
+ var clone_date = function(date, adjust)
+ {
+ var d = new Date(date.getTime());
+
+ // set time to 00:00
+ if (adjust == 1) {
+ d.setHours(0);
+ d.setMinutes(0);
+ }
+ // set time to 23:59
+ else if (adjust == 2) {
+ d.setHours(23);
+ d.setMinutes(59);
+ }
+
+ return d;
+ };
+
+ // fix date if jumped over a DST change
+ var fix_date = function(date)
+ {
+ if (date.getHours() == 23)
+ date.setTime(date.getTime() + HOUR_MS);
+ else if (date.getHours() > 0)
+ date.setHours(0);
+ };
+
+ var date2timestring = function(date, dateonly)
+ {
+ return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14));
+ }
+
+ var format_datetime = function(date, mode, voice)
+ {
+ return me.format_datetime(date, mode, voice);
+ }
+
+ var render_link = function(url)
+ {
+ var islink = false, href = url;
+ if (url.match(/^[fhtpsmailo]+?:\/\//i)) {
+ islink = true;
+ }
+ else if (url.match(/^[a-z0-9.-:]+(\/|$)/i)) {
+ islink = true;
+ href = 'http://' + url;
+ }
+ return islink ? '<a href="' + Q(href) + '" target="_blank">' + Q(url) + '</a>' : Q(url);
+ }
+
+ // determine whether the given date is on a weekend
+ var is_weekend = function(date)
+ {
+ return date.getDay() == 0 || date.getDay() == 6;
+ };
+
+ var is_workinghour = function(date)
+ {
+ if (settings['work_start'] > settings['work_end'])
+ return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end'];
+ else
+ return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end'];
+ };
+
+ // check if the event has 'real' attendees, excluding the current user
+ var has_attendees = function(event)
+ {
+ return (event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email));
+ };
+
+ // check if the current user is an attendee of this event
+ var is_attendee = function(event, role, email)
+ {
+ var emails = email ? ';'+email.toLowerCase() : settings.identity.emails;
+ for (var i=0; event.attendees && i < event.attendees.length; i++) {
+ if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0)
+ return event.attendees[i];
+ }
+ return false;
+ };
+
+ // check if the current user is the organizer
+ var is_organizer = function(event, email)
+ {
+ return is_attendee(event, 'ORGANIZER', email) || !event.id;
+ };
+
+ /**
+ * Check permissions on the given calendar object
+ */
+ var has_permission = function(cal, perm)
+ {
+ // multiple chars means "either of"
+ if (String(perm).length > 1) {
+ for (var i=0; i < perm.length; i++) {
+ if (has_permission(cal, perm[i]))
+ return true;
+ }
+ }
+
+ if (cal.rights && String(cal.rights).indexOf(perm) >= 0) {
+ return true;
+ }
+
+ return (perm == 'i' && cal.editable) || (perm == 'v' && cal.editable);
+ }
+
+ var load_attachment = function(event, att)
+ {
+ var query = { _id: att.id, _event: event.recurrence_id || event.id, _cal:event.calendar, _frame: 1 };
+ if (event.rev)
+ query._rev = event.rev;
+
+ // open attachment in frame if it's of a supported mimetype
+ if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
+ if (rcmail.open_window(rcmail.url('get-attachment', query), true, true)) {
+ return;
+ }
+ }
+
+ query._frame = null;
+ query._download = 1;
+ rcmail.goto_url('get-attachment', query, false);
+ };
+
+ // build event attachments list
+ var event_show_attachments = function(list, container, event, edit)
+ {
+ var i, id, len, img, content, li, elem,
+ ul = document.createElement('UL');
+ ul.className = 'attachmentslist';
+
+ for (i=0, len=list.length; i<len; i++) {
+ elem = list[i];
+ li = document.createElement('LI');
+ li.className = elem.classname;
+
+ if (edit) {
+ rcmail.env.attachments[elem.id] = elem;
+ // delete icon
+ content = $('<a href="#delete" />')
+ .attr('title', rcmail.gettext('delete'))
+ .attr('aria-label', rcmail.gettext('delete') + ' ' + Q(elem.name))
+ .addClass('delete')
+ .click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
+
+ if (!rcmail.env.deleteicon)
+ content.html(rcmail.gettext('delete'));
+ else {
+ img = document.createElement('IMG');
+ img.src = rcmail.env.deleteicon;
+ img.alt = rcmail.gettext('delete');
+ content.append(img);
+ }
+
+ content.appendTo(li);
+ }
+
+ // name/link
+ content = $('<a href="#load" />')
+ .html(Q(elem.name))
+ .addClass('file')
+ .click({event: event, att: elem}, function(e) {
+ load_attachment(e.data.event, e.data.att);
+ return false;
+ })
+ .appendTo(li);
+
+ ul.appendChild(li);
+ }
+
+ if (edit && rcmail.gui_objects.attachmentlist) {
+ ul.id = rcmail.gui_objects.attachmentlist.id;
+ rcmail.gui_objects.attachmentlist = ul;
+ }
+
+ container.empty().append(ul);
+ };
+
+ var remove_attachment = function(elem, id)
+ {
+ $(elem.parentNode).hide();
+ rcmail.env.deleted_attachments.push(id);
+ delete rcmail.env.attachments[id];
+ };
+
+ // event details dialog (show only)
+ var event_show_dialog = function(event, ev, temp)
+ {
+ var $dialog = $("#eventshow");
+ var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false, rights:'lrs' };
+
+ if (!temp)
+ me.selected_event = event;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ // remove status-* classes
+ $dialog.removeClass(function(i, oldclass) {
+ var oldies = String(oldclass).split(' ');
+ return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 || cls.indexOf('sensitivity-') === 0 }).join(' ');
+ });
+
+ // convert start/end dates if not done yet by fullcalendar
+ if (typeof event.start == 'string')
+ event.start = parseISO8601(event.start);
+ if (typeof event.end == 'string')
+ event.end = parseISO8601(event.end);
+
+ // allow other plugins to do actions when event form is opened
+ rcmail.triggerEvent('calendar-event-init', {o: event});
+
+ $dialog.find('div.event-section, div.event-line').hide();
+ $('#event-title').html(Q(event.title)).show();
+
+ if (event.location)
+ $('#event-location').html('@ ' + text2html(event.location)).show();
+ if (event.description)
+ $('#event-description').show().children('.event-text').html(text2html(event.description, 300, 6));
+ if (event.vurl)
+ $('#event-url').show().children('.event-text').html(render_link(event.vurl));
+
+ // render from-to in a nice human-readable way
+ // -> now shown in dialog title
+ // $('#event-date').html(Q(me.event_date_text(event))).show();
+
+ if (event.recurrence && event.recurrence_text)
+ $('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text));
+
+ if (event.valarms && event.alarms_text)
+ $('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
+
+ if (calendar.name)
+ $('#event-calendar').show().children('.event-text').html(Q(calendar.name)).attr('class', 'event-text cal-'+calendar.id).css('color', calendar.textColor || calendar.color || '');
+ if (event.categories)
+ $('#event-category').show().children('.event-text').html(Q(event.categories)).attr('class', 'event-text cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, ''));
+ if (event.free_busy)
+ $('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
+ if (event.priority > 0) {
+ var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+ $('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority]));
+ }
+
+ if (event.status) {
+ var status_lc = String(event.status).toLowerCase();
+ $('#event-status').show().children('.event-text').html(Q(rcmail.gettext('status-'+status_lc,'calendar')));
+ $dialog.addClass('status-'+status_lc);
+ }
+ if (event.sensitivity && event.sensitivity != 'public') {
+ $('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
+ $dialog.addClass('sensitivity-'+event.sensitivity);
+ }
+ if (event.created || event.changed) {
+ var created = parseISO8601(event.created),
+ changed = parseISO8601(event.changed)
+ $('#event-created-changed .event-created').html(Q(created ? format_datetime(created) : rcmail.gettext('unknown','calendar')))
+ $('#event-created-changed .event-changed').html(Q(changed ? format_datetime(changed) : rcmail.gettext('unknown','calendar')))
+ $('#event-created-changed').show()
+ }
+
+ // create attachments list
+ if ($.isArray(event.attachments)) {
+ event_show_attachments(event.attachments, $('#event-attachments').children('.event-text'), event);
+ if (event.attachments.length > 0) {
+ $('#event-attachments').show();
+ }
+ }
+ else if (calendar.attachments) {
+ // fetch attachments, some drivers doesn't set 'attachments' prop of the event?
+ }
+
+ // build attachments list
+ $('#event-links').hide();
+ if ($.isArray(event.links) && event.links.length) {
+ render_message_links(event.links || [], $('#event-links').children('.event-text'), false, 'calendar');
+ $('#event-links').show();
+ }
+
+ // list event attendees
+ if (calendar.attendees && event.attendees) {
+ // sort resources to the end
+ event.attendees.sort(function(a,b) {
+ var j = a.cutype == 'RESOURCE' ? 1 : 0,
+ k = b.cutype == 'RESOURCE' ? 1 : 0;
+ return (j - k);
+ });
+
+ var data, mystatus = null, rsvp, line, morelink, html = '', overflow = '';
+ for (var j=0; j < event.attendees.length; j++) {
+ data = event.attendees[j];
+ if (data.email) {
+ if (data.role == 'ORGANIZER')
+ organizer = true;
+ else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
+ mystatus = data.status.toLowerCase();
+ if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
+ rsvp = mystatus;
+ }
+ }
+
+ line = event_attendee_html(data);
+
+ if (morelink)
+ overflow += line;
+ else
+ html += line;
+
+ // stop listing attendees
+ if (j == 7 && event.attendees.length >= 7) {
+ morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1));
+ }
+ }
+
+ if (html && (event.attendees.length > 1 || !organizer)) {
+ $('#event-attendees').show()
+ .children('.event-text')
+ .html(html)
+ .find('a.mailtolink').click(event_attendee_click);
+
+ // display all attendees in a popup when clicking the "more" link
+ if (morelink) {
+ $('#event-attendees .event-text').append(morelink);
+ morelink.click(function(e){
+ rcmail.show_popup_dialog(
+ '<div id="all-event-attendees" class="event-attendees">' + html + overflow + '</div>',
+ rcmail.gettext('tabattendees','calendar'),
+ null,
+ { width:450, modal:false });
+ $('#all-event-attendees a.mailtolink').click(event_attendee_click);
+ return false;
+ })
+ }
+ }
+
+ if (mystatus && !rsvp) {
+ $('#event-partstat').show().children('.changersvp')
+ .removeClass('accepted tentative declined delegated needs-action')
+ .addClass(mystatus)
+ .children('.event-text')
+ .html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring')));
+ }
+
+ var show_rsvp = rsvp && !is_organizer(event) && event.status != 'CANCELLED' && has_permission(calendar, 'v');
+ $('#event-rsvp')[(show_rsvp ? 'show' : 'hide')]();
+ $('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true);
+
+ if (show_rsvp && event.comment)
+ $('#event-rsvp-comment').show().children('.event-text').html(Q(event.comment));
+
+ $('#event-rsvp a.reply-comment-toggle').show();
+ $('#event-rsvp .itip-reply-comment textarea').hide().val('');
+
+ if (event.recurrence && event.id) {
+ var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
+ $('#event-rsvp .rsvp-buttons').addClass('recurring');
+ }
+ else {
+ $('#event-rsvp .rsvp-buttons').removeClass('recurring');
+ }
+ }
+
+ var buttons = [];
+ if (!temp && calendar.editable && event.editable !== false) {
+ buttons.push({
+ text: rcmail.gettext('edit', 'calendar'),
+ click: function() {
+ event_edit_dialog('edit', event);
+ }
+ });
+ }
+ if (!temp && has_permission(calendar, 'td') && event.editable !== false) {
+ buttons.push({
+ text: rcmail.gettext('delete', 'calendar'),
+ 'class': 'delete',
+ click: function() {
+ me.delete_event(event);
+ $dialog.dialog('close');
+ }
+ });
+ }
+
+ if (!buttons.length) {
+ buttons.push({
+ text: rcmail.gettext('close', 'calendar'),
+ click: function(){
+ $dialog.dialog('close');
+ }
+ });
+ }
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: !bw.ie6,
+ closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons
+ title: me.event_date_text(event),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
+ },
+ dragStart: function() {
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
+ },
+ resizeStart: function() {
+ rcmail.command('menu-close','eventoptionsmenu');
+ $('.libcal-rsvp-replymode').hide();
+ },
+ buttons: buttons,
+ minWidth: 320,
+ width: 420
+ }).show();
+
+ // remember opener element (to be focused on close)
+ $dialog.data('opener', ev && rcube_event.is_keyboard(ev) ? ev.target : null);
+
+ // set voice title on dialog widget
+ $dialog.dialog('widget').removeAttr('aria-labelledby')
+ .attr('aria-label', me.event_date_text(event, true) + ', ', event.title);
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 420);
+
+ // add link for "more options" drop-down
+ if (!temp && !event.temporary && event.calendar != '_resource') {
+ $('<a>')
+ .attr('href', '#')
+ .html(rcmail.gettext('eventoptions','calendar'))
+ .addClass('dropdown-link')
+ .click(function(e) {
+ return rcmail.command('menu-open','eventoptionsmenu', this, e)
+ })
+ .appendTo($dialog.parent().find('.ui-dialog-buttonset'));
+ }
+
+ rcmail.enable_command('event-history', calendar.history)
+ };
+
+ // render HTML code for displaying an attendee record
+ var event_attendee_html = function(data)
+ {
+ var dispname = Q(data.name || data.email), tooltip = '';
+
+ if (data.email) {
+ tooltip = data.email + '; ' + data.status;
+ dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+ }
+
+ if (data['delegated-to'])
+ tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
+ else if (data['delegated-from'])
+ tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
+
+ return '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+ };
+
+ // event handler for clicks on an attendee link
+ var event_attendee_click = function(e)
+ {
+ var cutype = $(this).attr('data-cutype'),
+ mailto = this.href.substr(7);
+ if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
+ event_resources_dialog(mailto);
+ }
+ else {
+ rcmail.command('compose', mailto, e ? e.target : null, e);
+ }
+ return false;
+ };
+
+ // bring up the event dialog (jquery-ui popup)
+ var event_edit_dialog = function(action, event)
+ {
+ // copy opener element from show dialog
+ var op_elem = $("#eventshow:ui-dialog").data('opener');
+
+ // close show dialog first
+ $("#eventshow:ui-dialog").data('opener', null).dialog('close');
+
+ var $dialog = $('<div>');
+ var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:true, rights: action=='new' ? 'lrwitd' : 'lrs' };
+ me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults)
+ event = me.selected_event; // change reference to clone
+ freebusy_ui.needsupdate = false;
+
+ // reset dialog first
+ $('#eventtabs').get(0).reset();
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', false);
+ $('#event-panel-recurrence, #event-panel-attachments').removeClass('disabled');
+
+ // allow other plugins to do actions when event form is opened
+ rcmail.triggerEvent('calendar-event-init', {o: event});
+
+ // event details
+ var title = $('#edit-title').val(event.title || '');
+ var location = $('#edit-location').val(event.location || '');
+ var description = $('#edit-description').text(event.description || '');
+ var vurl = $('#edit-url').val(event.vurl || '');
+ var categories = $('#edit-categories').val(event.categories);
+ var calendars = $('#edit-calendar').val(event.calendar);
+ var eventstatus = $('#edit-event-status').val(event.status);
+ var freebusy = $('#edit-free-busy').val(event.free_busy);
+ var priority = $('#edit-priority').val(event.priority);
+ var sensitivity = $('#edit-sensitivity').val(event.sensitivity);
+
+ var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
+ var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
+ var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
+ var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
+ var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
+ var allday = $('#edit-allday').get(0);
+ var notify = $('#edit-attendees-donotify').get(0);
+ var invite = $('#edit-attendees-invite').get(0);
+ var comment = $('#edit-attendees-comment');
+
+ invite.checked = settings.itip_notify & 1 > 0;
+ notify.checked = has_attendees(event) && invite.checked;
+
+ if (event.allDay) {
+ starttime.val("12:00").hide();
+ endtime.val("13:00").hide();
+ allday.checked = true;
+ }
+ else {
+ allday.checked = false;
+ }
+
+ // set calendar selection according to permissions
+ calendars.find('option').each(function(i, opt) {
+ var cal = me.calendars[opt.value] || {};
+ $(opt).prop('disabled', !(cal.editable || (action == 'new' && has_permission(cal, 'i'))))
+ });
+
+ // set alarm(s)
+ me.set_alarms_edit('#edit-alarms', action != 'new' && event.valarms && calendar.alarms ? event.valarms : []);
+
+ // enable/disable alarm property according to backend support
+ $('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')]();
+
+ // check categories drop-down: add value if not exists
+ if (event.categories && !categories.find("option[value='"+event.categories+"']").length) {
+ $('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
+ }
+
+ if ($.isArray(event.links) && event.links.length) {
+ render_message_links(event.links, $('#edit-event-links .event-text'), true, 'calendar');
+ $('#edit-event-links').show();
+ }
+ else {
+ $('#edit-event-links').hide();
+ }
+
+ // show warning if editing a recurring event
+ if (event.id && event.recurrence) {
+ var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
+ $('#edit-recurring-warning').show();
+ $('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true).change();
+ }
+ else
+ $('#edit-recurring-warning').hide();
+
+ // init attendees tab
+ var organizer = !event.attendees || is_organizer(event),
+ allow_invitations = organizer || (calendar.owner && calendar.owner == 'anonymous') || settings.invite_shared;
+ event_attendees = [];
+ attendees_list = $('#edit-attendees-table > tbody').html('');
+ resources_list = $('#edit-resources-table > tbody').html('');
+ $('#edit-attendees-notify')[(allow_invitations && has_attendees(event) && (settings.itip_notify & 2) ? 'show' : 'hide')]();
+ $('#edit-localchanges-warning')[(has_attendees(event) && !(allow_invitations || (calendar.owner && is_organizer(event, calendar.owner))) ? 'show' : 'hide')]();
+
+ var load_attendees_tab = function()
+ {
+ var j, data, reply_selected = 0;
+ if (event.attendees) {
+ for (j=0; j < event.attendees.length; j++) {
+ data = event.attendees[j];
+ // reset attendee status
+ if (event._savemode == 'new' && data.role != 'ORGANIZER') {
+ data.status = 'NEEDS-ACTION';
+ delete data.noreply;
+ }
+ add_attendee(data, !allow_invitations);
+ if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply)
+ reply_selected++;
+ }
+ }
+
+ // make sure comment box is visible if at least one attendee has reply enabled
+ // or global "send invitations" checkbox is checked
+ $('#eventedit .attendees-commentbox')[(reply_selected || invite.checked ? 'show' : 'hide')]();
+
+ // select the correct organizer identity
+ var identity_id = 0;
+ $.each(settings.identities, function(i,v){
+ if (organizer && typeof organizer == 'object' && v == organizer.email) {
+ identity_id = i;
+ return false;
+ }
+ });
+ $('#edit-identities-list').val(identity_id);
+ $('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
+ $('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
+ };
+
+ // attachments
+ var load_attachments_tab = function()
+ {
+ rcmail.enable_command('remove-attachment', calendar.editable && !event.recurrence_id);
+ rcmail.env.deleted_attachments = [];
+ // we're sharing some code for uploads handling with app.js
+ rcmail.env.attachments = [];
+ rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
+
+ if ($.isArray(event.attachments)) {
+ event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
+ }
+ else {
+ $('#edit-attachments > ul').empty();
+ // fetch attachments, some drivers doesn't set 'attachments' array for event?
+ }
+ };
+
+ // init dialog buttons
+ var buttons = [];
+
+ // save action
+ buttons.push({
+ text: rcmail.gettext('save', 'calendar'),
+ 'class': 'mainaction',
+ click: function() {
+ var start = parse_datetime(allday.checked ? '12:00' : starttime.val(), startdate.val());
+ var end = parse_datetime(allday.checked ? '13:00' : endtime.val(), enddate.val());
+
+ // basic input validatetion
+ if (start.getTime() > end.getTime()) {
+ alert(rcmail.gettext('invalideventdates', 'calendar'));
+ return false;
+ }
+
+ // post data to server
+ var data = {
+ calendar: event.calendar,
+ start: date2servertime(start),
+ end: date2servertime(end),
+ allday: allday.checked?1:0,
+ title: title.val(),
+ description: description.val(),
+ location: location.val(),
+ categories: categories.val(),
+ vurl: vurl.val(),
+ free_busy: freebusy.val(),
+ priority: priority.val(),
+ sensitivity: sensitivity.val(),
+ status: eventstatus.val(),
+ recurrence: me.serialize_recurrence(endtime.val()),
+ valarms: me.serialize_alarms('#edit-alarms'),
+ attendees: event_attendees,
+ links: me.selected_event.links,
+ deleted_attachments: rcmail.env.deleted_attachments,
+ attachments: []
+ };
+
+ // uploaded attachments list
+ for (var i in rcmail.env.attachments)
+ if (i.match(/^rcmfile(.+)/))
+ data.attachments.push(RegExp.$1);
+
+ // read attendee roles
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (data.attendees[i])
+ data.attendees[i].role = $(elem).val();
+ });
+
+ if (organizer)
+ data._identity = $('#edit-identities-list option:selected').val();
+
+ // don't submit attendees if only myself is added as organizer
+ if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email)
+ data.attendees = [];
+
+ // per-attendee notification suppression
+ var need_invitation = false;
+ if (allow_invitations) {
+ $.each(data.attendees, function (i, v) {
+ if (v.role != 'ORGANIZER') {
+ if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked') || v.cutype == 'RESOURCE') {
+ need_invitation = true;
+ delete data.attendees[i]['noreply'];
+ }
+ else if (settings.itip_notify > 0) {
+ data.attendees[i].noreply = 1;
+ }
+ }
+ });
+ }
+
+ // tell server to send notifications
+ if ((data.attendees.length || (event.id && event.attendees.length)) && allow_invitations && (notify.checked || invite.checked || need_invitation)) {
+ data._notify = settings.itip_notify;
+ data._comment = comment.val();
+ }
+
+ data.calendar = calendars.val();
+
+ if (event.id) {
+ data.id = event.id;
+ if (event.recurrence)
+ data._savemode = $('input.edit-recurring-savemode:checked').val();
+ if (data.calendar && data.calendar != event.calendar)
+ data._fromcalendar = event.calendar;
+ }
+
+ update_event(action, data);
+ $dialog.dialog("close");
+ } // end click:
+ });
+
+ if (event.id) {
+ buttons.push({
+ text: rcmail.gettext('delete', 'calendar'),
+ 'class': 'delete',
+ click: function() {
+ me.delete_event(event);
+ $dialog.dialog('close');
+ }
+ });
+ }
+
+ buttons.push({
+ text: rcmail.gettext('cancel', 'calendar'),
+ click: function() {
+ $dialog.dialog("close");
+ }
+ });
+
+ // show/hide tabs according to calendar's feature support
+ $('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
+ $('#edit-tab-resources')[(rcmail.env.calendar_resources?'show':'hide')]();
+ $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
+
+ // activate the first tab
+ $('#eventtabs').tabs('option', 'active', 0);
+
+ // hack: set task to 'calendar' to make all dialog actions work correctly
+ var comm_path_before = rcmail.env.comm_path;
+ rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar');
+
+ var editform = $("#eventedit");
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
+ closeOnEscape: false,
+ title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
+ open: function() {
+ editform.attr('aria-hidden', 'false');
+ },
+ close: function() {
+ editform.hide().attr('aria-hidden', 'true').appendTo(document.body);
+ $dialog.dialog("destroy").remove();
+ rcmail.ksearch_blur();
+ freebusy_data = {};
+ rcmail.env.comm_path = comm_path_before; // restore comm_path
+ if (op_elem)
+ $(op_elem).focus();
+ },
+ buttons: buttons,
+ minWidth: 500,
+ width: 600
+ }).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
+
+ // set dialog size according to form content
+ me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 550);
+
+ title.select();
+
+ // init other tabs asynchronously
+ window.setTimeout(function(){ me.set_recurrence_edit(event); }, exec_deferred);
+ if (calendar.attendees)
+ window.setTimeout(load_attendees_tab, exec_deferred);
+ if (calendar.attachments)
+ window.setTimeout(load_attachments_tab, exec_deferred);
+ };
+
+ // show event changelog in a dialog
+ var event_history_dialog = function(event)
+ {
+ if (!event.id || !window.libkolab_audittrail)
+ return false
+
+ // render dialog
+ var $dialog = libkolab_audittrail.object_history_dialog({
+ module: 'calendar',
+ container: '#eventhistory',
+ title: rcmail.gettext('objectchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
+
+ // callback function for list actions
+ listfunc: function(action, rev) {
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:action, e:{ id:event.id, calendar:event.calendar, rev: rev } }, me.loading_lock);
+ },
+
+ // callback function for comparing two object revisions
+ comparefunc: function(rev1, rev2) {
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'diff', e:{ id:event.id, calendar:event.calendar, rev1: rev1, rev2: rev2 } }, me.loading_lock);
+ }
+ });
+
+ $dialog.data('event', event);
+
+ // fetch changelog data
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_post('event', { action:'changelog', e:{ id:event.id, calendar:event.calendar } }, me.loading_lock);
+ };
+
+ // callback from server with changelog data
+ var render_event_changelog = function(data)
+ {
+ var $dialog = $('#eventhistory'),
+ event = $dialog.data('event');
+
+ if (data === false || !data.length || !event) {
+ // display 'unavailable' message
+ $('<div class="notfound-message event-dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','calendar') + '</div>')
+ .insertBefore($dialog.find('.changelog-table').hide());
+ return;
+ }
+
+ data.module = 'calendar';
+ libkolab_audittrail.render_changelog(data, event, me.calendars[event.calendar]);
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 600);
+ };
+
+ // callback from server with event diff data
+ var event_show_diff = function(data)
+ {
+ var event = me.selected_event,
+ $dialog = $("#eventdiff");
+
+ $dialog.find('div.event-section, div.event-line, h1.event-title-new').hide().data('set', false).find('.index').html('');
+ $dialog.find('div.event-section.clone, div.event-line.clone').remove();
+
+ // always show event title and date
+ $('.event-title', $dialog).text(event.title).removeClass('event-text-old').show();
+ $('.event-date', $dialog).text(me.event_date_text(event)).show();
+
+ // show each property change
+ $.each(data.changes, function(i,change) {
+ var prop = change.property, r2, html = false,
+ row = $('div.event-' + prop, $dialog).first();
+
+ // special case: title
+ if (prop == 'title') {
+ $('.event-title', $dialog).addClass('event-text-old').text(change['old'] || '--');
+ $('.event-title-new', $dialog).text(change['new'] || '--').show();
+ }
+
+ // no display container for this property
+ if (!row.length) {
+ return true;
+ }
+
+ // clone row if already exists
+ if (row.data('set')) {
+ r2 = row.clone().addClass('clone').insertAfter(row);
+ row = r2;
+ }
+
+ // format dates
+ if (['start','end','changed'].indexOf(prop) >= 0) {
+ if (change['old']) change.old_ = me.format_datetime(parseISO8601(change['old']));
+ if (change['new']) change.new_ = me.format_datetime(parseISO8601(change['new']));
+ }
+ // render description text
+ else if (prop == 'description') {
+ // TODO: show real text diff
+ if (!change.diff_ && change['old']) change.old_ = text2html(change['old']);
+ if (!change.diff_ && change['new']) change.new_ = text2html(change['new']);
+ html = true;
+ }
+ // format attendees struct
+ else if (prop == 'attendees') {
+ if (change['old']) change.old_ = event_attendee_html(change['old']);
+ if (change['new']) change.new_ = event_attendee_html($.extend({}, change['old'] || {}, change['new']));
+ html = true;
+ }
+ // localize priority values
+ else if (prop == 'priority') {
+ var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+ if (change['old']) change.old_ = change['old'] + ' ' + (priolabels[change['old']] || '');
+ if (change['new']) change.new_ = change['new'] + ' ' + (priolabels[change['new']] || '');
+ }
+ // localize status
+ else if (prop == 'status') {
+ var status_lc = String(event.status).toLowerCase();
+ if (change['old']) change.old_ = rcmail.gettext(String(change['old']).toLowerCase(), 'calendar');
+ if (change['new']) change.new_ = rcmail.gettext(String(change['new']).toLowerCase(), 'calendar');
+ }
+
+ // format attachments struct
+ if (prop == 'attachments') {
+ if (change['old']) event_show_attachments([change['old']], row.children('.event-text-old'), event, false);
+ else row.children('.event-text-old').text('--');
+ if (change['new']) event_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.event-text-new'), event, false);
+ else row.children('.event-text-new').text('--');
+ // remove click handler as we're currentyl not able to display the according attachment contents
+ $('.attachmentslist li a', row).unbind('click').removeAttr('href');
+ }
+ else if (change.diff_) {
+ row.children('.event-text-diff').html(change.diff_);
+ row.children('.event-text-old, .event-text-new').hide();
+ }
+ else {
+ if (!html) {
+ // escape HTML characters
+ change.old_ = Q(change.old_ || change['old'] || '--')
+ change.new_ = Q(change.new_ || change['new'] || '--')
+ }
+ row.children('.event-text-old').html(change.old_ || change['old'] || '--');
+ row.children('.event-text-new').html(change.new_ || change['new'] || '--');
+ }
+
+ // display index number
+ if (typeof change.index != 'undefined') {
+ row.find('.index').html('(' + change.index + ')');
+ }
+
+ row.show().data('set', true);
+
+ // hide event-date line
+ if (prop == 'start' || prop == 'end')
+ $('.event-date', $dialog).hide();
+ });
+
+ var buttons = {};
+ buttons[rcmail.gettext('close', 'calendar')] = function() {
+ $dialog.dialog('close');
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: false,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('objectdiff','calendar').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + event.title,
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ buttons: buttons,
+ minWidth: 320,
+ width: 450
+ }).show();
+
+ // set dialog size according to content
+ me.dialog_resize($dialog.get(0), $dialog.height(), 400);
+ };
+
+ // close the event history dialog
+ var close_history_dialog = function()
+ {
+ $('#eventhistory, #eventdiff').each(function(i, elem) {
+ var $dialog = $(elem);
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+ });
+ }
+
+ // exports
+ this.event_show_diff = event_show_diff;
+ this.event_show_dialog = event_show_dialog;
+ this.event_history_dialog = event_history_dialog;
+ this.render_event_changelog = render_event_changelog;
+ this.close_history_dialog = close_history_dialog;
+
+ // open a dialog to display detailed free-busy information and to find free slots
+ var event_freebusy_dialog = function()
+ {
+ var $dialog = $('#eventfreebusy'),
+ event = me.selected_event;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (!event_attendees.length)
+ return false;
+
+ // set form elements
+ var allday = $('#edit-allday').get(0);
+ var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
+ freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
+ freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
+ freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
+ freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
+
+ if (allday.checked) {
+ freebusy_ui.starttime.val("12:00").hide();
+ freebusy_ui.endtime.val("13:00").hide();
+ event.allDay = true;
+ }
+
+ // read attendee roles from drop-downs
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (event_attendees[i])
+ event_attendees[i].role = $(elem).val();
+ });
+
+ // render time slots
+ var now = new Date(), fb_start = new Date(), fb_end = new Date();
+ fb_start.setTime(event.start);
+ fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
+ fb_end.setTime(fb_start.getTime() + DAY_MS);
+
+ freebusy_data = { required:{}, all:{} };
+ freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet
+ freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400));
+ freebusy_ui.interval = allday.checked ? 1440 : 60;
+ freebusy_ui.start = fb_start;
+ freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
+ render_freebusy_grid(0);
+
+ // render list of attendees
+ freebusy_ui.attendees = {};
+ var domid, dispname, data, role_html, list_html = '';
+ for (var i=0; i < event_attendees.length; i++) {
+ data = event_attendees[i];
+ dispname = Q(data.name || data.email);
+ domid = String(data.email).replace(rcmail.identifier_expr, '');
+ role_html = '<a class="attendee-role-toggle" id="rcmlia' + domid + '" title="' + Q(rcmail.gettext('togglerole', 'calendar')) + '">&nbsp;</a>';
+ list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + role_html + dispname + '</div>';
+
+ // clone attendees data for local modifications
+ freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data);
+ }
+
+ // add total row
+ list_html += '<div class="attendee spacer">&nbsp;</div>';
+ list_html += '<div class="attendee total">' + rcmail.gettext('reqallattendees','calendar') + '</div>';
+
+ $('#schedule-attendees-list').html(list_html)
+ .unbind('click.roleicons')
+ .bind('click.roleicons', function(e){
+ // toggle attendee status upon click on icon
+ if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
+ var attendee, domid = RegExp.$1,
+ roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'NON-PARTICIPANT', 'CHAIR' ];
+ if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
+ var req = attendee.role != 'OPT-PARTICIPANT' && attendee.role != 'NON-PARTICIPANT';
+ var j = $.inArray(attendee.role, roles);
+ j = (j+1) % roles.length;
+ attendee.role = roles[j];
+ $(e.target).parent().attr('class', 'attendee '+String(attendee.role).toLowerCase());
+
+ // update total display if required-status changed
+ if (req != (roles[j] != 'OPT-PARTICIPANT' && roles[j] != 'NON-PARTICIPANT')) {
+ compute_freebusy_totals();
+ update_freebusy_display(attendee.email);
+ }
+ }
+ }
+
+ return false;
+ });
+
+ // enable/disable buttons
+ $('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
+
+ // dialog buttons
+ var buttons = {};
+
+ buttons[rcmail.gettext('select', 'calendar')] = function() {
+ $('#edit-startdate').val(freebusy_ui.startdate.val());
+ $('#edit-starttime').val(freebusy_ui.starttime.val());
+ $('#edit-enddate').val(freebusy_ui.enddate.val());
+ $('#edit-endtime').val(freebusy_ui.endtime.val());
+
+ // write role changes back to main dialog
+ $('select.edit-attendee-role').each(function(i, elem){
+ if (event_attendees[i] && freebusy_ui.attendees[i]) {
+ event_attendees[i].role = freebusy_ui.attendees[i].role;
+ $(elem).val(event_attendees[i].role);
+ }
+ });
+
+ if (freebusy_ui.needsupdate)
+ update_freebusy_status(me.selected_event);
+ freebusy_ui.needsupdate = false;
+ $dialog.dialog("close");
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ $dialog.dialog({
+ modal: true,
+ resizable: true,
+ closeOnEscape: (!bw.ie6 && !bw.ie7),
+ title: rcmail.gettext('scheduletime', 'calendar'),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false').find('#shedule-find-next, #shedule-find-prev').not(':disabled').first().focus();
+ },
+ close: function() {
+ if (bw.ie6)
+ $("#edit-attendees-table").css('visibility','visible');
+ $dialog.dialog("destroy").attr('aria-hidden', 'true').hide();
+ // TODO: focus opener button
+ },
+ resizeStop: function() {
+ render_freebusy_overlay();
+ },
+ buttons: buttons,
+ minWidth: 640,
+ width: 850
+ }).show();
+
+ // hide edit dialog on IE6 because of drop-down elements
+ if (bw.ie6)
+ $("#edit-attendees-table").css('visibility','hidden');
+
+ // adjust dialog size to fit grid without scrolling
+ var gridw = $('#schedule-freebusy-times').width();
+ var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
+ me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow));
+
+ // fetch data from server
+ freebusy_ui.loading = 0;
+ load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
+ };
+
+ // render an HTML table showing free-busy status for all the event attendees
+ var render_freebusy_grid = function(delta)
+ {
+ if (delta) {
+ freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
+ fix_date(freebusy_ui.start);
+
+ // skip weekends if in workinhoursonly-mode
+ if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) {
+ while (is_weekend(freebusy_ui.start))
+ freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
+ fix_date(freebusy_ui.start);
+ }
+
+ freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
+ }
+
+ var dayslots = Math.floor(1440 / freebusy_ui.interval);
+ var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format);
+ var lastdate, datestr, css,
+ curdate = new Date(),
+ allday = (freebusy_ui.interval == 1440),
+ times_css = (allday ? 'allday ' : ''),
+ dates_row = '<tr class="dates">',
+ times_row = '<tr class="times">',
+ slots_row = '';
+ for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) {
+ curdate.setTime(t);
+ datestr = fc.fullCalendar('formatDate', curdate, date_format);
+ if (datestr != lastdate) {
+ if (lastdate && !allday) break;
+ dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + $.fullCalendar.formatDate(curdate, 'ddMMyyyy') + '">' + Q(datestr) + '</th>';
+ lastdate = datestr;
+ }
+
+ // set css class according to working hours
+ css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours';
+ times_row += '<td class="' + times_css + css + '" id="t-' + Math.floor(t/1000) + '">' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
+ slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
+
+ t += freebusy_ui.interval * 60000;
+ }
+ dates_row += '</tr>';
+ times_row += '</tr>';
+
+ // render list of attendees
+ var domid, data, list_html = '', times_html = '';
+ for (var i=0; i < event_attendees.length; i++) {
+ data = event_attendees[i];
+ domid = String(data.email).replace(rcmail.identifier_expr, '');
+ times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
+ }
+
+ // add line for all/required attendees
+ times_html += '<tr class="spacer"><td colspan="' + (dayslots * freebusy_ui.numdays) + '">&nbsp;</td>';
+ times_html += '<tr id="fbrowall">' + slots_row + '</tr>';
+
+ var table = $('#schedule-freebusy-times');
+ table.children('thead').html(dates_row + times_row);
+ table.children('tbody').html(times_html);
+
+ // initialize event handlers on grid
+ if (!freebusy_ui.grid_events) {
+ freebusy_ui.grid_events = true;
+ table.children('thead').click(function(e){
+ // move event to the clicked date/time
+ if (e.target.id && e.target.id.match(/t-(\d+)/)) {
+ var newstart = new Date(RegExp.$1 * 1000);
+ // set time to 00:00
+ if (me.selected_event.allDay) {
+ newstart.setMinutes(0);
+ newstart.setHours(0);
+ }
+ update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
+ render_freebusy_overlay();
+ }
+ })
+ }
+
+ // if we have loaded free-busy data, show it
+ if (!freebusy_ui.loading) {
+ if (freebusy_ui.start < freebusy_data.start || freebusy_ui.end > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) {
+ load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
+ }
+ else {
+ for (var email, i=0; i < event_attendees.length; i++) {
+ if ((email = event_attendees[i].email))
+ update_freebusy_display(email);
+ }
+ }
+ }
+
+ // render current event date/time selection over grid table
+ // use timeout to let the dom attributes (width/height/offset) be set first
+ window.setTimeout(function(){ render_freebusy_overlay(); }, 10);
+ };
+
+ // render overlay element over the grid to visiualize the current event date/time
+ var render_freebusy_overlay = function()
+ {
+ var overlay = $('#schedule-event-time');
+ if (me.selected_event.end.getTime() <= freebusy_ui.start.getTime() || me.selected_event.start.getTime() >= freebusy_ui.end.getTime()) {
+ overlay.hide();
+ if (overlay.data('isdraggable'))
+ overlay.draggable('disable');
+ }
+ else {
+ var table = $('#schedule-freebusy-times'),
+ width = 0,
+ pos = { top:table.children('thead').height(), left:0 },
+ eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)),
+ eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60,
+ slotstart = date2unixtime(freebusy_ui.start),
+ slotsize = freebusy_ui.interval * 60,
+ slotend, fraction, $cell;
+
+ // iterate through slots to determine position and size of the overlay
+ table.children('thead').find('td').each(function(i, cell){
+ slotend = slotstart + slotsize - 1;
+ // event starts in this slot: compute left
+ if (eventstart >= slotstart && eventstart <= slotend) {
+ fraction = 1 - (slotend - eventstart) / slotsize;
+ pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction);
+ }
+ // event ends in this slot: compute width
+ if (eventend >= slotstart && eventend <= slotend) {
+ fraction = 1 - (slotend - eventend) / slotsize;
+ width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left;
+ }
+
+ slotstart = slotstart + slotsize;
+ });
+
+ if (!width)
+ width = table.width() - pos.left;
+
+ // overlay is visible
+ if (width > 0) {
+ overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).show();
+
+ // configure draggable
+ if (!overlay.data('isdraggable')) {
+ overlay.draggable({
+ axis: 'x',
+ scroll: true,
+ stop: function(e, ui){
+ // convert pixels to time
+ var px = ui.position.left;
+ var range_p = $('#schedule-freebusy-times').width();
+ var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime();
+ var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p));
+ newstart.setSeconds(0); newstart.setMilliseconds(0);
+ // snap to day boundaries
+ if (me.selected_event.allDay) {
+ if (newstart.getHours() >= 12) // snap to next day
+ newstart.setTime(newstart.getTime() + DAY_MS);
+ newstart.setMinutes(0);
+ newstart.setHours(0);
+ }
+ else {
+ // round to 5 minutes
+ var round = newstart.getMinutes() % 5;
+ if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000);
+ else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000);
+ }
+ // update event times and display
+ update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
+ if (me.selected_event.allDay)
+ render_freebusy_overlay();
+ }
+ }).data('isdraggable', true);
+ }
+ else
+ overlay.draggable('enable');
+ }
+ else
+ overlay.draggable('disable').hide();
+ }
+
+ };
+
+
+ // fetch free-busy information for each attendee from server
+ var load_freebusy_data = function(from, interval)
+ {
+ var start = new Date(from.getTime() - DAY_MS * 2); // start 2 days before event
+ fix_date(start);
+ var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days
+ freebusy_ui.numrequired = 0;
+ freebusy_data.all = [];
+ freebusy_data.required = [];
+
+ // load free-busy information for every attendee
+ var domid, email;
+ for (var i=0; i < event_attendees.length; i++) {
+ if ((email = event_attendees[i].email)) {
+ domid = String(email).replace(rcmail.identifier_expr, '');
+ $('#rcmli' + domid).addClass('loading');
+ freebusy_ui.loading++;
+
+ $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ url: rcmail.url('freebusy-times'),
+ data: { email:email, start:date2servertime(clone_date(start, 1)), end:date2servertime(clone_date(end, 2)), interval:interval, _remote:1 },
+ success: function(data) {
+ freebusy_ui.loading--;
+
+ // find attendee
+ var attendee = null;
+ for (var i=0; i < event_attendees.length; i++) {
+ if (freebusy_ui.attendees[i].email == data.email) {
+ attendee = freebusy_ui.attendees[i];
+ break;
+ }
+ }
+
+ // copy data to member var
+ var ts, req = attendee.role != 'OPT-PARTICIPANT';
+ freebusy_data.start = parseISO8601(data.start);
+ freebusy_data[data.email] = {};
+ for (var i=0; i < data.slots.length; i++) {
+ ts = data.times[i] + '';
+ freebusy_data[data.email][ts] = data.slots[i];
+
+ // set totals
+ if (!freebusy_data.required[ts])
+ freebusy_data.required[ts] = [0,0,0,0];
+ if (req)
+ freebusy_data.required[ts][data.slots[i]]++;
+
+ if (!freebusy_data.all[ts])
+ freebusy_data.all[ts] = [0,0,0,0];
+ freebusy_data.all[ts][data.slots[i]]++;
+ }
+ freebusy_data.end = parseISO8601(data.end);
+ freebusy_data.interval = data.interval;
+
+ // hide loading indicator
+ var domid = String(data.email).replace(rcmail.identifier_expr, '');
+ $('#rcmli' + domid).removeClass('loading');
+
+ // update display
+ update_freebusy_display(data.email);
+ }
+ });
+
+ // count required attendees
+ if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT')
+ freebusy_ui.numrequired++;
+ }
+ }
+ };
+
+ // re-calculate total status after role change
+ var compute_freebusy_totals = function()
+ {
+ freebusy_ui.numrequired = 0;
+ freebusy_data.all = [];
+ freebusy_data.required = [];
+
+ var email, req, status;
+ for (var i=0; i < event_attendees.length; i++) {
+ if (!(email = event_attendees[i].email))
+ continue;
+
+ req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT';
+ if (req)
+ freebusy_ui.numrequired++;
+
+ for (var ts in freebusy_data[email]) {
+ if (!freebusy_data.required[ts])
+ freebusy_data.required[ts] = [0,0,0,0];
+ if (!freebusy_data.all[ts])
+ freebusy_data.all[ts] = [0,0,0,0];
+
+ status = freebusy_data[email][ts];
+ freebusy_data.all[ts][status]++;
+
+ if (req)
+ freebusy_data.required[ts][status]++;
+ }
+ }
+ };
+
+ // update free-busy grid with status loaded from server
+ var update_freebusy_display = function(email)
+ {
+ var status_classes = ['unknown','free','busy','tentative','out-of-office'];
+ var domid = String(email).replace(rcmail.identifier_expr, '');
+ var row = $('#fbrow' + domid);
+ var rowall = $('#fbrowall').children();
+ var dateonly = freebusy_ui.interval > 60,
+ t, ts = date2timestring(freebusy_ui.start, dateonly),
+ curdate = new Date(),
+ fbdata = freebusy_data[email];
+
+ if (fbdata && fbdata[ts] !== undefined && row.length) {
+ t = freebusy_ui.start.getTime();
+ row.children().each(function(i, cell){
+ curdate.setTime(t);
+ ts = date2timestring(curdate, dateonly);
+ cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown');
+
+ // also update total row if all data was loaded
+ if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) {
+ var workinghours = cell.className.indexOf('workinghours') >= 0;
+ var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown';
+ req_status = freebusy_data.required[ts][2] ? 'busy' : 'free';
+ for (var j=1; j < status_classes.length; j++) {
+ if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired)
+ req_status = status_classes[j];
+ if (freebusy_data.all[ts][j] == event_attendees.length)
+ all_status = status_classes[j];
+ }
+
+ cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status;
+ }
+
+ t += freebusy_ui.interval * 60000;
+ });
+ }
+ };
+
+ // write changed event date/times back to form fields
+ var update_freebusy_dates = function(start, end)
+ {
+ // fix all-day evebt times
+ if (me.selected_event.allDay) {
+ var numdays = Math.floor((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / DAY_MS);
+ start.setHours(12);
+ start.setMinutes(0);
+ end.setTime(start.getTime() + numdays * DAY_MS);
+ end.setHours(13);
+ end.setMinutes(0);
+ }
+ me.selected_event.start = start;
+ me.selected_event.end = end;
+ freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format']));
+ freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format']));
+ freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format']));
+ freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format']));
+ freebusy_ui.needsupdate = true;
+ };
+
+ // attempt to find a time slot where all attemdees are available
+ var freebusy_find_slot = function(dir)
+ {
+ // exit if free-busy data isn't available yet
+ if (!freebusy_data || !freebusy_data.start)
+ return false;
+
+ var event = me.selected_event,
+ eventstart = clone_date(event.start, event.allDay ? 1 : 0).getTime(), // calculate with integers
+ eventend = clone_date(event.end, event.allDay ? 2 : 0).getTime(),
+ duration = eventend - eventstart - (event.allDay ? HOUR_MS : 0), /* make sure we don't cross day borders on DST change */
+ sinterval = freebusy_data.interval * 60000,
+ intvlslots = 1,
+ numslots = Math.ceil(duration / sinterval),
+ checkdate, slotend, email, ts, slot, slotdate = new Date();
+
+ // shift event times to next possible slot
+ eventstart += sinterval * intvlslots * dir;
+ eventend += sinterval * intvlslots * dir;
+
+ // iterate through free-busy slots and find candidates
+ var candidatecount = 0, candidatestart = candidateend = success = false;
+ for (slot = dir > 0 ? freebusy_data.start.getTime() : freebusy_data.end.getTime() - sinterval;
+ (dir > 0 && slot < freebusy_data.end.getTime()) || (dir < 0 && slot >= freebusy_data.start.getTime());
+ slot += sinterval * dir) {
+ slotdate.setTime(slot);
+ // fix slot if just crossed a DST change
+ if (event.allDay) {
+ fix_date(slotdate);
+ slot = slotdate.getTime();
+ }
+ slotend = slot + sinterval;
+
+ if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
+ continue;
+
+ // respect workingours setting
+ if (freebusy_ui.workinhoursonly) {
+ if (is_weekend(slotdate) || (freebusy_data.interval <= 60 && !is_workinghour(slotdate))) { // skip off-hours
+ candidatestart = candidateend = false;
+ candidatecount = 0;
+ continue;
+ }
+ }
+
+ if (!candidatestart)
+ candidatestart = slot;
+
+ // check freebusy data for all attendees
+ ts = date2timestring(slotdate, freebusy_data.interval > 60);
+ for (var i=0; i < event_attendees.length; i++) {
+ if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][ts] > 1) {
+ candidatestart = candidateend = false;
+ break;
+ }
+ }
+
+ // occupied slot
+ if (!candidatestart) {
+ slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
+ candidatecount = 0;
+ continue;
+ }
+
+ // set candidate end to slot end time
+ candidatecount++;
+ if (dir < 0 && !candidateend)
+ candidateend = slotend;
+
+ // if candidate is big enough, this is it!
+ if (candidatecount == numslots) {
+ if (dir > 0) {
+ event.start.setTime(candidatestart);
+ event.end.setTime(candidatestart + duration);
+ }
+ else {
+ event.end.setTime(candidateend);
+ event.start.setTime(candidateend - duration);
+ }
+ success = true;
+ break;
+ }
+ }
+
+ // update event date/time display
+ if (success) {
+ update_freebusy_dates(event.start, event.end);
+
+ // move freebusy grid if necessary
+ var offset = Math.ceil((event.start.getTime() - freebusy_ui.end.getTime()) / DAY_MS);
+ if (event.start.getTime() >= freebusy_ui.end.getTime())
+ render_freebusy_grid(Math.max(1, offset));
+ else if (event.end.getTime() <= freebusy_ui.start.getTime())
+ render_freebusy_grid(Math.min(-1, offset));
+ else
+ render_freebusy_overlay();
+
+ var now = new Date();
+ $('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
+
+ // speak new selection
+ rcmail.display_message(rcmail.gettext('suggestedslot', 'calendar') + ': ' + me.event_date_text(event, true), 'voice');
+ }
+ else {
+ alert(rcmail.gettext('noslotfound','calendar'));
+ }
+ };
+
+
+ // update event properties and attendees availability if event times have changed
+ var event_times_changed = function()
+ {
+ if (me.selected_event) {
+ var allday = $('#edit-allday').get(0);
+ me.selected_event.allDay = allday.checked;
+ me.selected_event.start = parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
+ me.selected_event.end = parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val());
+ if (event_attendees)
+ freebusy_ui.needsupdate = true;
+ $('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
+ }
+ };
+
+
+ // add the given list of participants
+ var add_attendees = function(names, params)
+ {
+ names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
+
+ // parse name/email pairs
+ var item, email, name, success = false;
+ for (var i=0; i < names.length; i++) {
+ email = name = '';
+ item = $.trim(names[i]);
+
+ if (!item.length) {
+ continue;
+ } // address in brackets without name (do nothing)
+ else if (item.match(/^<[^@]+@[^>]+>$/)) {
+ email = item.replace(/[<>]/g, '');
+ } // address without brackets and without name (add brackets)
+ else if (rcube_check_email(item)) {
+ email = item;
+ } // address with name
+ else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
+ email = RegExp.$1;
+ name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
+ }
+ if (email) {
+ add_attendee($.extend({ email:email, name:name }, params));
+ success = true;
+ }
+ else {
+ alert(rcmail.gettext('noemailwarning'));
+ }
+ }
+
+ return success;
+ };
+
+ // add the given attendee to the list
+ var add_attendee = function(data, readonly, before)
+ {
+ if (!me.selected_event)
+ return false;
+
+ // check for dupes...
+ var exists = false;
+ $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
+ if (exists)
+ return false;
+
+ var calendar = me.selected_event && me.calendars[me.selected_event.calendar] ? me.calendars[me.selected_event.calendar] : me.calendars[me.selected_calendar];
+
+ var dispname = Q(data.name || data.email);
+ if (data.email)
+ dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+
+ // role selection
+ var organizer = data.role == 'ORGANIZER';
+ var opts = {};
+ if (organizer)
+ opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
+ opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
+ opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
+ opts['NON-PARTICIPANT'] = rcmail.gettext('calendar.rolenonparticipant');
+
+ if (data.cutype != 'RESOURCE')
+ opts['CHAIR'] = rcmail.gettext('calendar.rolechair');
+
+ if (organizer && !readonly)
+ dispname = rcmail.env['identities-selector'];
+
+ var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','calendar') + '">';
+ for (var r in opts)
+ select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
+ select += '</select>';
+
+ // availability
+ var avail = data.email ? 'loading' : 'unknown';
+
+ // delete icon
+ var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
+ var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
+ var tooltip = data.status || '';
+
+ // send invitation checkbox
+ var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('calendar.sendinvitations')) + '" '
+ + (!data.noreply && settings.itip_notify & 1 ? 'checked="checked" ' : '') + '/>';
+
+ if (data['delegated-to'])
+ tooltip = rcmail.gettext('delegatedto', 'calendar') + data['delegated-to'];
+ else if (data['delegated-from'])
+ tooltip = rcmail.gettext('delegatedfrom', 'calendar') + data['delegated-from'];
+
+ // add expand button for groups
+ if (data.cutype == 'GROUP') {
+ dispname += ' <a href="#expand" data-email="' + Q(data.email) + '" class="iconbutton add expandlink" title="' + rcmail.gettext('expandattendeegroup','libcalendaring') + '">' +
+ rcmail.gettext('expandattendeegroup','libcalendaring') + '</a>';
+ }
+
+ var img_src = rcmail.assets_path('program/resources/blank.gif');
+ var html = '<td class="role">' + select + '</td>' +
+ '<td class="name"><span class="attendee-name">' + dispname + '</span></td>' +
+ '<td class="availability"><img src="' + img_src + '" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
+ '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
+ (data.cutype != 'RESOURCE' ? '<td class="invite">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
+ '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
+
+ var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+ var tr = $('<tr>')
+ .addClass(String(data.role).toLowerCase())
+ .html(html);
+
+ if (before)
+ tr.insertBefore(before)
+ else
+ tr.appendTo(table);
+
+ tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+ tr.find('a.mailtolink').click(event_attendee_click);
+ tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); return false; });
+ tr.find('input.edit-attendee-reply').click(function() {
+ var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
+ $('#eventedit .attendees-commentbox')[enabled ? 'show' : 'hide']();
+ });
+
+ // select organizer identity
+ if (data.identity_id)
+ $('#edit-identities-list').val(data.identity_id);
+
+ // check free-busy status
+ if (avail == 'loading') {
+ check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event);
+ }
+
+ event_attendees.push(data);
+ return true;
+ };
+
+ // iterate over all attendees and update their free-busy status display
+ var update_freebusy_status = function(event)
+ {
+ attendees_list.find('img.availabilityicon').each(function(i,v) {
+ var email, icon = $(this);
+ if (email = icon.attr('data-email'))
+ check_freebusy_status(icon, email, event);
+ });
+
+ freebusy_ui.needsupdate = false;
+ };
+
+ // load free-busy status from server and update icon accordingly
+ var check_freebusy_status = function(icon, email, event)
+ {
+ var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
+ if (!calendar.freebusy) {
+ $(icon).attr('class', 'availabilityicon unknown');
+ return;
+ }
+
+ icon = $(icon).attr('class', 'availabilityicon loading');
+
+ $.ajax({
+ type: 'GET',
+ dataType: 'html',
+ url: rcmail.url('freebusy-status'),
+ data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
+ success: function(status){
+ var avail = String(status).toLowerCase();
+ icon.removeClass('loading').addClass(avail).attr('alt', rcmail.gettext('avail' + avail, 'calendar'));
+ },
+ error: function(){
+ icon.removeClass('loading').addClass('unknown').attr('alt', rcmail.gettext('availunknown', 'calendar'));
+ }
+ });
+ };
+
+ // remove an attendee from the list
+ var remove_attendee = function(elem, id)
+ {
+ $(elem).closest('tr').remove();
+ event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
+ };
+
+ // open a dialog to display detailed free-busy information and to find free slots
+ var event_resources_dialog = function(search)
+ {
+ var $dialog = $('#eventresourcesdialog');
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ // dialog buttons
+ var buttons = {};
+
+ buttons[rcmail.gettext('addresource', 'calendar')] = function() {
+ rcmail.command('add-resource');
+ };
+
+ buttons[rcmail.gettext('close')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('findresources', 'calendar'),
+ open: function() {
+ $dialog.attr('aria-hidden', 'false');
+ },
+ close: function() {
+ $dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
+ },
+ resize: function(e) {
+ var container = $(rcmail.gui_objects.resourceinfocalendar);
+ container.fullCalendar('option', 'height', container.height() + 4);
+ },
+ buttons: buttons,
+ width: 900,
+ height: 500
+ }).show();
+
+ // define add-button as main action
+ $('.ui-dialog-buttonset .ui-button', $dialog.parent()).first().addClass('mainaction').attr('id', 'rcmbtncalresadd');
+
+ me.dialog_resize($dialog.get(0), 540, Math.min(1000, $(window).width() - 50));
+
+ // set search query
+ $('#resourcesearchbox').val(search || '');
+
+ // initialize the treelist widget
+ if (!resources_treelist) {
+ resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, {
+ id_prefix: 'rcres',
+ id_encode: rcmail.html_identifier_encode,
+ id_decode: rcmail.html_identifier_decode,
+ selectable: true,
+ save_state: true
+ });
+ resources_treelist.addEventListener('select', function(node) {
+ if (resources_data[node.id]) {
+ resource_showinfo(resources_data[node.id]);
+ rcmail.enable_command('add-resource', me.selected_event && $("#eventedit").is(':visible') ? true : false);
+ }
+ else {
+ rcmail.enable_command('add-resource', false);
+ $(rcmail.gui_objects.resourceinfo).hide();
+ $(rcmail.gui_objects.resourceownerinfo).hide();
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source);
+ }
+ });
+
+ // fetch (all) resource data from server
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_request('resources-list', {}, me.loading_lock);
+
+ // register button
+ rcmail.register_button('add-resource', 'rcmbtncalresadd', 'uibutton');
+
+ // initialize resource calendar display
+ var resource_cal = $(rcmail.gui_objects.resourceinfocalendar);
+ resource_cal.fullCalendar($.extend({}, fullcalendar_defaults, {
+ header: { left: '', center: '', right: '' },
+ height: resource_cal.height() + 4,
+ defaultView: 'agendaWeek',
+ eventSources: [],
+ slotMinutes: 60,
+ allDaySlot: false,
+ eventRender: function(event, element, view) {
+ var title = rcmail.get_label(event.status, 'calendar');
+ element.addClass('status-' + event.status);
+ element.find('.fc-event-head').hide();
+ element.find('.fc-event-title').text(title);
+ element.attr('aria-label', me.event_date_text(event, true) + ': ' + title);
+ }
+ }));
+
+ $('#resource-calendar-prev').click(function(){
+ resource_cal.fullCalendar('prev');
+ return false;
+ });
+ $('#resource-calendar-next').click(function(){
+ resource_cal.fullCalendar('next');
+ return false;
+ });
+ }
+ else if (search) {
+ resource_search();
+ }
+ else {
+ resource_render_list(resources_index);
+ }
+
+ if (me.selected_event && me.selected_event.start) {
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('gotoDate', me.selected_event.start);
+ }
+ };
+
+ // render the resource details UI box
+ var resource_showinfo = function(resource)
+ {
+ // inline function to render a resource attribute
+ function render_attrib(value) {
+ if (typeof value == 'boolean') {
+ return value ? rcmail.get_label('yes') : rcmail.get_label('no');
+ }
+
+ return value;
+ }
+
+ if (rcmail.gui_objects.resourceinfo) {
+ var tr, table = $(rcmail.gui_objects.resourceinfo).show().find('tbody').html(''),
+ attribs = $.extend({ name:resource.name }, resource.attributes||{})
+ attribs.description = resource.description;
+
+ for (var k in attribs) {
+ if (typeof attribs[k] == 'undefined')
+ continue;
+ table.append($('<tr>').addClass(k)
+ .append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
+ .append('<td class="value">' + text2html(render_attrib(attribs[k])) + '</td>')
+ );
+ }
+
+ $(rcmail.gui_objects.resourceownerinfo).hide();
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSource', resources_events_source);
+
+ if (resource.owner) {
+ // display cached data
+ if (resource_owners[resource.owner]) {
+ resource_owner_load(resource_owners[resource.owner]);
+ }
+ else {
+ // fetch owner data from server
+ me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
+ rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
+ }
+ }
+
+ // load resource calendar
+ resources_events_source.url = "./?_task=calendar&_action=resources-calendar&_id="+urlencode(resource.ID);
+ $(rcmail.gui_objects.resourceinfocalendar).fullCalendar('addEventSource', resources_events_source);
+ }
+ };
+
+ // callback from server for resource listing
+ var resource_data_load = function(data)
+ {
+ var resources_tree = {};
+
+ // store data by ID
+ $.each(data, function(i, rec) {
+ resources_data[rec.ID] = rec;
+
+ // assign parent-relations
+ if (rec.members) {
+ $.each(rec.members, function(j, m){
+ resources_tree[m] = rec.ID;
+ });
+ }
+ });
+
+ // walk the parent-child tree to determine the depth of each node
+ $.each(data, function(i, rec) {
+ rec._depth = 0;
+ if (resources_tree[rec.ID])
+ rec.parent_id = resources_tree[rec.ID];
+
+ var parent_id = resources_tree[rec.ID];
+ while (parent_id) {
+ rec._depth++;
+ parent_id = resources_tree[parent_id];
+ }
+ });
+
+ // sort by depth, collection and name
+ data.sort(function(a,b) {
+ var j = a._type == 'collection' ? 1 : 0,
+ k = b._type == 'collection' ? 1 : 0,
+ d = a._depth - b._depth;
+ if (!d) d = (k - j);
+ if (!d) d = b.name < a.name ? 1 : -1;
+ return d;
+ });
+
+ $.each(data, function(i, rec) {
+ resources_index.push(rec.ID);
+ });
+
+ // apply search filter...
+ if ($('#resourcesearchbox').val() != '')
+ resource_search();
+ else // ...or render full list
+ resource_render_list(resources_index);
+
+ rcmail.set_busy(false, null, me.loading_lock);
+ };
+
+ // renders the given list of resource records into the treelist
+ var resource_render_list = function(index) {
+ var rec, link;
+
+ resources_treelist.reset();
+
+ $.each(index, function(i, dn) {
+ if (rec = resources_data[dn]) {
+ link = $('<a>').attr('href', '#')
+ .attr('rel', rec.ID)
+ .html(Q(rec.name));
+
+ resources_treelist.insert({ id:rec.ID, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false);
+ }
+ });
+ };
+
+ // callback from server for owner information display
+ var resource_owner_load = function(data)
+ {
+ if (data) {
+ // cache this!
+ resource_owners[data.ID] = data;
+
+ var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html('');
+
+ for (var k in data) {
+ if (k == 'event' || k == 'ID')
+ continue;
+
+ table.append($('<tr>').addClass(k)
+ .append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
+ .append('<td class="value">' + text2html(data[k]) + '</td>')
+ );
+ }
+
+ table.parent().show();
+ }
+ }
+
+ // quick-filter the loaded resource data
+ var resource_search = function()
+ {
+ var dn, rec, dataset = [],
+ q = $('#resourcesearchbox').val().toLowerCase();
+
+ if (q.length && resources_data) {
+ // search by iterating over all resource records
+ for (dn in resources_data) {
+ rec = resources_data[dn];
+ if ((rec.name && String(rec.name).toLowerCase().indexOf(q) >= 0)
+ || (rec.email && String(rec.email).toLowerCase().indexOf(q) >= 0)
+ || (rec.description && String(rec.description).toLowerCase().indexOf(q) >= 0)
+ ) {
+ dataset.push(rec.ID);
+ }
+ }
+
+ resource_render_list(dataset);
+
+ // select single match
+ if (dataset.length == 1) {
+ resources_treelist.select(dataset[0]);
+ }
+ }
+ else {
+ $('#resourcesearchbox').val('');
+ }
+ };
+
+ //
+ var reset_resource_search = function()
+ {
+ $('#resourcesearchbox').val('').focus();
+ resource_render_list(resources_index);
+ };
+
+ //
+ var add_resource2event = function()
+ {
+ var resource = resources_data[resources_treelist.get_selection()];
+ if (resource) {
+ if (add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource)))
+ rcmail.display_message(rcmail.get_label('resourceadded', 'calendar'), 'confirmation');
+ }
+ }
+
+ // when the user accepts or declines an event invitation
+ var event_rsvp = function(response, delegate, replymode)
+ {
+ var btn;
+ if (typeof response == 'object') {
+ btn = $(response);
+ response = btn.attr('rel')
+ }
+ else {
+ btn = $('#event-rsvp input.button[rel='+response+']');
+ }
+
+ // show menu to select rsvp reply mode (current or all)
+ if (me.selected_event && me.selected_event.recurrence && !replymode) {
+ rcube_libcalendaring.itip_rsvp_recurring(btn, function(resp, mode) {
+ event_rsvp(resp, null, mode);
+ });
+ return;
+ }
+
+ if (me.selected_event && me.selected_event.attendees && response) {
+ // bring up delegation dialog
+ if (response == 'delegated' && !delegate) {
+ rcube_libcalendaring.itip_delegate_dialog(function(data) {
+ data.rsvp = data.rsvp ? 1 : '';
+ event_rsvp('delegated', data, replymode);
+ });
+ return;
+ }
+
+ // update attendee status
+ attendees = [];
+ for (var data, i=0; i < me.selected_event.attendees.length; i++) {
+ data = me.selected_event.attendees[i];
+ if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
+ data.status = response.toUpperCase();
+ data.rsvp = 0; // unset RSVP flag
+
+ if (data.status == 'DELEGATED') {
+ data['delegated-to'] = delegate.to;
+ data.rsvp = delegate.rsvp
+ }
+ else {
+ if (data['delegated-to']) {
+ delete data['delegated-to'];
+ if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED')
+ data.role = 'REQ-PARTICIPANT';
+ }
+ }
+
+ attendees.push(i)
+ }
+ else if (response != 'DELEGATED' && data['delegated-from'] &&
+ settings.identity.emails.indexOf(';'+String(data['delegated-from']).toLowerCase()) >= 0) {
+ delete data['delegated-from'];
+ }
+
+ // set free_busy status to transparent if declined (#4425)
+ if (data.status == 'DECLINED' || data.role == 'NON-PARTICIPANT') {
+ me.selected_event.free_busy = 'free';
+ }
+ else {
+ me.selected_event.free_busy = 'busy';
+ }
+ }
+
+ // submit status change to server
+ var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: replymode || 'all' }, (delegate || {})),
+ noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
+
+ // import event from mail (temporary iTip event)
+ if (submit_data._mbox && submit_data._uid) {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('mailimportitip', {
+ _mbox: submit_data._mbox,
+ _uid: submit_data._uid,
+ _part: submit_data._part,
+ _status: response,
+ _to: (delegate ? delegate.to : null),
+ _rsvp: (delegate && delegate.rsvp) ? 1 : 0,
+ _noreply: noreply,
+ _comment: submit_data.comment,
+ _instance: submit_data._instance,
+ _savemode: submit_data._savemode
+ });
+ }
+ else if (settings.invitation_calendars) {
+ update_event('rsvp', submit_data, { status:response, noreply:noreply, attendees:attendees });
+ }
+ else {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('event', { action:'rsvp', e:submit_data, status:response, attendees:attendees, noreply:noreply });
+ }
+
+ event_show_dialog(me.selected_event);
+ }
+ };
+
+ // add the given date to the RDATE list
+ var add_rdate = function(date)
+ {
+ var li = $('<li>')
+ .attr('data-value', date2servertime(date))
+ .html('<span>' + Q($.fullCalendar.formatDate(date, settings['date_format'])) + '</span>')
+ .appendTo('#edit-recurrence-rdates');
+
+ $('<a>').attr('href', '#del')
+ .addClass('iconbutton delete')
+ .html(rcmail.get_label('delete', 'calendar'))
+ .attr('title', rcmail.get_label('delete', 'calendar'))
+ .appendTo(li);
+ };
+
+ // re-sort the list items by their 'data-value' attribute
+ var sort_rdates = function()
+ {
+ var mylist = $('#edit-recurrence-rdates'),
+ listitems = mylist.children('li').get();
+ listitems.sort(function(a, b) {
+ var compA = $(a).attr('data-value');
+ var compB = $(b).attr('data-value');
+ return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+ })
+ $.each(listitems, function(idx, item) { mylist.append(item); });
+ }
+
+ // remove the link reference matching the given uri
+ function remove_link(elem)
+ {
+ var $elem = $(elem), uri = $elem.attr('data-uri');
+
+ me.selected_event.links = $.grep(me.selected_event.links, function(link) { return link.uri != uri; });
+
+ // remove UI list item
+ $elem.hide().closest('li').addClass('deleted');
+ }
+
+ // post the given event data to server
+ var update_event = function(action, data, add)
+ {
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('calendar/event', $.extend({ action:action, e:data }, (add || {})));
+
+ // render event temporarily into the calendar
+ if ((data.start && data.end) || data.id) {
+ var event = data.id ? $.extend(fc.fullCalendar('clientEvents', function(e){ return e.id == data.id; })[0], data) : data;
+ if (data.start)
+ event.start = data.start;
+ if (data.end)
+ event.end = data.end;
+ if (data.allday !== undefined)
+ event.allDay = data.allday;
+ event.editable = false;
+ event.temp = true;
+ event.className = 'fc-event-cal-'+data.calendar+' fc-event-temp';
+ fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event);
+
+ // mark all recurring instances as temp
+ if (event.recurrence || event.recurrence_id) {
+ var base_id = event.recurrence_id ? event.recurrence_id : String(event.id).replace(/-\d+(T\d{6})?$/, '');
+ $.each(fc.fullCalendar('clientEvents', function(e){ return e.id == base_id || e.recurrence_id == base_id; }), function(i,ev) {
+ ev.temp = true;
+ ev.editable = false;
+ event.className += ' fc-event-temp';
+ fc.fullCalendar('updateEvent', ev);
+ });
+ }
+ }
+ };
+
+ // mouse-click handler to check if the show dialog is still open and prevent default action
+ var dialog_check = function(e)
+ {
+ var showd = $("#eventshow");
+ if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length && !$(e.target).closest('.popupmenu').length) {
+ showd.dialog('close');
+ e.stopImmediatePropagation();
+ ignore_click = true;
+ return false;
+ }
+ else if (ignore_click) {
+ window.setTimeout(function(){ ignore_click = false; }, 20);
+ return false;
+ }
+ return true;
+ };
+
+ // display confirm dialog when modifying/deleting an event
+ var update_event_confirm = function(action, event, data)
+ {
+ if (!data) data = event;
+ var decline = false, notify = false, html = '', cal = me.calendars[event.calendar],
+ _has_attendees = has_attendees(event), _is_organizer = is_organizer(event);
+
+ // event has attendees, ask whether to notify them
+ if (_has_attendees) {
+ var checked = (settings.itip_notify & 1 ? ' checked="checked"' : '');
+ if (_is_organizer) {
+ notify = true;
+ if (settings.itip_notify & 2) {
+ html += '<div class="message">' +
+ '<label><input class="confirm-attendees-donotify" type="checkbox"' + checked + ' value="1" name="notify" />&nbsp;' +
+ rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
+ '</label></div>';
+ }
+ else {
+ data._notify = settings.itip_notify;
+ }
+ }
+ else if (action == 'remove' && is_attendee(event)) {
+ decline = true;
+ checked = event.status != 'CANCELLED' ? checked : '';
+ html += '<div class="message">' +
+ '<label><input class="confirm-attendees-decline" type="checkbox"' + checked + ' value="1" name="decline" />&nbsp;' +
+ rcmail.gettext('itipdeclineevent', 'calendar') +
+ '</label></div>';
+ }
+ else {
+ html += '<div class="message">' + rcmail.gettext('localchangeswarning', 'calendar') + '</div>';
+ }
+ }
+
+ // recurring event: user needs to select the savemode
+ if (event.recurrence) {
+ var future_disabled = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
+
+ // disable the 'future' savemode if I'm an attendee
+ // reason: no calendaring system supports the thisandfuture range parameter in iTip REPLY
+ if (action == 'remove' && _has_attendees && !_is_organizer && is_attendee(event)) {
+ future_disabled = ' disabled';
+ }
+
+ html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
+ rcmail.gettext(message_label, 'calendar') + '</div>' +
+ '<div class="savemode">' +
+ '<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
+ '<a href="#future" class="button' + future_disabled + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
+ '<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
+ (action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
+ '</div>';
+ }
+
+ // show dialog
+ if (html) {
+ var $dialog = $('<div>').html(html);
+
+ $dialog.find('a.button').button().filter(':not(.disabled)').click(function(e) {
+ data._savemode = String(this.href).replace(/.+#/, '');
+ data._notify = settings.itip_notify;
+
+ // open event edit dialog when saving as new
+ if (data._savemode == 'new') {
+ event._savemode = 'new';
+ event_edit_dialog('edit', event);
+ fc.fullCalendar('refetchEvents');
+ }
+ else {
+ if ($dialog.find('input.confirm-attendees-donotify').length)
+ data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
+ if (decline) {
+ data._decline = $dialog.find('input.confirm-attendees-decline:checked').length;
+ data._notify = 0;
+ }
+ update_event(action, data);
+ }
+
+ $dialog.dialog("close");
+ return false;
+ });
+
+ var buttons = [];
+
+ if (!event.recurrence) {
+ buttons.push({
+ text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
+ click: function() {
+ data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
+ data._decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
+ update_event(action, data);
+ $(this).dialog("close");
+ }
+ });
+ }
+
+ buttons.push({
+ text: rcmail.gettext('cancel', 'calendar'),
+ click: function() {
+ $(this).dialog("close");
+ }
+ });
+
+ $dialog.dialog({
+ modal: true,
+ width: 460,
+ dialogClass: 'warning',
+ title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
+ buttons: buttons,
+ open: function() {
+ setTimeout(function(){
+ $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+ }, 5);
+ },
+ close: function(){
+ $dialog.dialog("destroy").remove();
+ if (!rcmail.busy)
+ fc.fullCalendar('refetchEvents');
+ }
+ }).addClass('event-update-confirm').show();
+
+ return false;
+ }
+ // show regular confirm box when deleting
+ else if (action == 'remove' && !cal.undelete) {
+ if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar')))
+ return false;
+ }
+
+ // do update
+ update_event(action, data);
+
+ return true;
+ };
+
+ var update_agenda_toolbar = function()
+ {
+ $('#agenda-listrange').val(fc.fullCalendar('option', 'listRange'));
+ $('#agenda-listsections').val(fc.fullCalendar('option', 'listSections'));
+ }
+
+
+ /*** public methods ***/
+
+ /**
+ * Remove saving lock and free the UI for new input
+ */
+ this.unlock_saving = function()
+ {
+ if (me.saving_lock)
+ rcmail.set_busy(false, null, me.saving_lock);
+ };
+
+ // opens calendar day-view in a popup
+ this.fisheye_view = function(date)
+ {
+ $('#fish-eye-view:ui-dialog').dialog('close');
+
+ // create list of active event sources
+ var src, cals = {}, sources = [];
+ for (var id in this.calendars) {
+ src = $.extend({}, this.calendars[id]);
+ src.editable = false;
+ src.url = null;
+ src.events = [];
+
+ if (src.active) {
+ cals[id] = src;
+ sources.push(src);
+ }
+ }
+
+ // copy events already loaded
+ var events = fc.fullCalendar('clientEvents');
+ for (var event, i=0; i< events.length; i++) {
+ event = events[i];
+ if (event.source && (src = cals[event.source.id])) {
+ src.events.push(event);
+ }
+ }
+
+ var h = $(window).height() - 50;
+ var dialog = $('<div>')
+ .attr('id', 'fish-eye-view')
+ .dialog({
+ modal: true,
+ width: 680,
+ height: h,
+ title: $.fullCalendar.formatDate(date, 'dddd ' + settings['date_long']),
+ close: function(){
+ dialog.dialog("destroy");
+ me.fisheye_date = null;
+ }
+ })
+ .fullCalendar($.extend({}, fullcalendar_defaults, {
+ defaultView: 'agendaDay',
+ header: { left: '', center: '', right: '' },
+ height: h - 50,
+ date: date.getDate(),
+ month: date.getMonth(),
+ year: date.getFullYear(),
+ eventSources: sources
+ }));
+
+ this.fisheye_date = date;
+ };
+
+ // opens the given calendar in a popup dialog
+ this.quickview = function(id, shift)
+ {
+ var src, in_quickview = false;
+ $.each(this.quickview_sources, function(i,cal) {
+ if (cal.id == id) {
+ in_quickview = true;
+ src = cal;
+ }
+ });
+
+ // remove source from quickview
+ if (in_quickview && shift) {
+ this.quickview_sources = $.grep(this.quickview_sources, function(src) { return src.id != id; });
+ }
+ else {
+ if (!shift) {
+ // remove all current quickview event sources
+ if (this.quickview_active) {
+ fc.fullCalendar('removeEventSources');
+ }
+
+ this.quickview_sources = [];
+
+ // uncheck all active quickview icons
+ calendars_list.container.find('div.focusview')
+ .add('#calendars .searchresults div.focusview')
+ .removeClass('focusview')
+ .find('a.quickview').attr('aria-checked', 'false');
+ }
+
+ if (!in_quickview) {
+ // clone and modify calendar properties
+ src = $.extend({}, this.calendars[id]);
+ src.url += '&_quickview=1';
+ this.quickview_sources.push(src);
+ }
+ }
+
+ // disable quickview
+ if (this.quickview_active && !this.quickview_sources.length) {
+ // register regular calendar event sources
+ $.each(this.calendars, function(k, cal) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', cal);
+ });
+
+ this.quickview_active = false;
+ $('body').removeClass('quickview-active');
+
+ // uncheck all active quickview icons
+ calendars_list.container.find('div.focusview')
+ .add('#calendars .searchresults div.focusview')
+ .removeClass('focusview')
+ .find('a.quickview').attr('aria-checked', 'false');
+ }
+ // activate quickview
+ else if (!this.quickview_active) {
+ // remove regular calendar event sources
+ fc.fullCalendar('removeEventSources');
+
+ // register quickview event sources
+ $.each(this.quickview_sources, function(i, src) {
+ fc.fullCalendar('addEventSource', src);
+ });
+
+ this.quickview_active = true;
+ $('body').addClass('quickview-active');
+ }
+ // update quickview sources
+ else if (in_quickview) {
+ fc.fullCalendar('removeEventSource', src);
+ }
+ else if (src) {
+ fc.fullCalendar('addEventSource', src);
+ }
+
+ // activate quickview icon
+ if (this.quickview_active) {
+ $(calendars_list.get_item(id)).find('.calendar').first()
+ .add('#calendars .searchresults .cal-' + id)
+ [in_quickview ? 'removeClass' : 'addClass']('focusview')
+ .find('a.quickview').attr('aria-checked', in_quickview ? 'false' : 'true');
+ }
+ };
+
+ // disable quickview mode
+ function reset_quickview()
+ {
+ // remove all current quickview event sources
+ if (me.quickview_active) {
+ fc.fullCalendar('removeEventSources');
+ me.quickview_sources = [];
+ }
+
+ // register regular calendar event sources
+ $.each(me.calendars, function(k, cal) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', cal);
+ });
+
+ // uncheck all active quickview icons
+ calendars_list.container.find('div.focusview')
+ .add('#calendars .searchresults div.focusview')
+ .removeClass('focusview')
+ .find('a.quickview').attr('aria-checked', 'false');
+
+ me.quickview_active = false;
+ $('body').removeClass('quickview-active');
+ };
+
+ //public method to show the print dialog.
+ this.print_calendars = function(view)
+ {
+ if (!view) view = fc.fullCalendar('getView').name;
+ var date = fc.fullCalendar('getDate') || new Date();
+ var range = fc.fullCalendar('option', 'listRange');
+ var sections = fc.fullCalendar('option', 'listSections');
+ rcmail.open_window(rcmail.url('print', { view: view, date: date2unixtime(date), range: range, sections: sections, search: this.search_query }), true, true);
+ };
+
+ // public method to bring up the new event dialog
+ this.add_event = function(templ) {
+ if (this.selected_calendar) {
+ var now = new Date();
+ var date = fc.fullCalendar('getDate');
+ if (typeof date != 'Date')
+ date = now;
+ date.setHours(now.getHours()+1);
+ date.setMinutes(0);
+ var end = new Date(date.getTime());
+ end.setHours(date.getHours()+1);
+ event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {}));
+ }
+ };
+
+ // delete the given event after showing a confirmation dialog
+ this.delete_event = function(event) {
+ // show confirm dialog for recurring events, use jquery UI dialog
+ return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
+ };
+
+ // opens a jquery UI dialog with event properties (or empty for creating a new calendar)
+ this.calendar_edit_dialog = function(calendar)
+ {
+ // close show dialog first
+ var $dialog = $("#calendarform");
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (!calendar)
+ calendar = { name:'', color:'cc0000', editable:true, showalarms:true };
+
+ var form, name, color, alarms;
+
+ $dialog.html(rcmail.get_label('loading'));
+ $.ajax({
+ type: 'GET',
+ dataType: 'html',
+ url: rcmail.url('calendar'),
+ data: { action:(calendar.id ? 'form-edit' : 'form-new'), c:{ id:calendar.id }, driver: calendar.driver },
+ success: function(data) {
+ $dialog.html(data);
+ // resize and reposition dialog window
+ form = $('#calendarpropform');
+ me.dialog_resize('#calendarform', form.height(), form.width());
+ name = $('#calendar-name').prop('disabled', !calendar.editable && !calendar.editable_name).val(calendar.editname || calendar.name);
+ color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color, colorValues:rcmail.env.mscolors });
+ alarms = $('#calendar-showalarms').prop('checked', calendar.showalarms).get(0);
+ name.select();
+ }
+ });
+
+ // dialog buttons
+ var buttons = {};
+
+ buttons[rcmail.gettext('save', 'calendar')] = function() {
+ // form is not loaded
+ if (!form || !form.length)
+ return;
+
+ // TODO: do some input validation
+ if (!name.val() || name.val().length < 2) {
+ alert(rcmail.gettext('invalidcalendarproperties', 'calendar'));
+ name.select();
+ return;
+ }
+
+ // post data to server
+ var data = form.serializeJSON();
+ if (data.color)
+ data.color = data.color.replace(/^#/, '');
+ if (calendar.id)
+ data.id = calendar.id;
+ if (alarms)
+ data.showalarms = alarms.checked ? 1 : 0;
+
+ me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
+ rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data, driver: calendar.driver });
+ $dialog.dialog("close");
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: true,
+ closeOnEscape: false,
+ title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
+ open: function() {
+ $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+ },
+ close: function() {
+ $dialog.html('').dialog("destroy").hide();
+ },
+ buttons: buttons,
+ minWidth: 400,
+ width: 420
+ }).show();
+
+ };
+
+ this.calendar_remove = function(calendar)
+ {
+ this.calendar_destroy_source(calendar.id);
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:calendar.id, active:0, permanent:0, recursive:1 } });
+ return true;
+ };
+
+ this.calendar_delete = function(calendar)
+ {
+ if (confirm(rcmail.gettext(calendar.children ? 'deletecalendarconfirmrecursive' : 'deletecalendarconfirm', 'calendar'))) {
+ rcmail.http_post('calendar', { action:'delete', c:{ id:calendar.id }, driver: calendar.driver });
+ return true;
+ }
+ return false;
+ };
+
+ this.calendar_destroy_source = function(id)
+ {
+ var delete_ids = [];
+
+ if (this.calendars[id]) {
+ // find sub-calendars
+ if (this.calendars[id].children) {
+ for (var child_id in this.calendars) {
+ if (String(child_id).indexOf(id) == 0)
+ delete_ids.push(child_id);
+ }
+ }
+ else {
+ delete_ids.push(id);
+ }
+ }
+
+ // delete all calendars in the list
+ for (var i=0; i < delete_ids.length; i++) {
+ id = delete_ids[i];
+ calendars_list.remove(id);
+ fc.fullCalendar('removeEventSource', this.calendars[id]);
+ $('#edit-calendar option[value="'+id+'"]').remove();
+ delete this.calendars[id];
+ }
+ };
+
+ // open a dialog to upload an .ics file with events to be imported
+ this.import_events = function(calendar)
+ {
+ // close show dialog first
+ var $dialog = $("#eventsimport"),
+ form = rcmail.gui_objects.importform;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (calendar)
+ $('#event-import-calendar').val(calendar.id);
+
+ var buttons = {};
+ buttons[rcmail.gettext('import', 'calendar')] = function() {
+ if (form && form.elements._data.value) {
+ rcmail.async_upload_form(form, 'import_events', function(e) {
+ rcmail.set_busy(false, null, me.saving_lock);
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+
+ // display error message if no sophisticated response from server arrived (e.g. iframe load error)
+ if (me.import_succeeded === null)
+ rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error');
+ });
+
+ // display upload indicator (with extended timeout)
+ var timeout = rcmail.env.request_timeout;
+ rcmail.env.request_timeout = 600;
+ me.import_succeeded = null;
+ me.saving_lock = rcmail.set_busy(true, 'uploading');
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('disable');
+
+ // restore settings
+ rcmail.env.request_timeout = timeout;
+ }
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: false,
+ closeOnEscape: false,
+ title: rcmail.gettext('importevents', 'calendar'),
+ open: function() {
+ $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+ },
+ close: function() {
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+ $dialog.dialog("destroy").hide();
+ },
+ buttons: buttons,
+ width: 520
+ }).show();
+
+ };
+
+ // callback from server if import succeeded
+ this.import_success = function(p)
+ {
+ this.import_succeeded = true;
+ $("#eventsimport:ui-dialog").dialog('close');
+ rcmail.set_busy(false, null, me.saving_lock);
+ rcmail.gui_objects.importform.reset();
+
+ if (p.refetch)
+ this.refresh(p);
+ };
+
+ // callback from server to report errors on import
+ this.import_error = function(p)
+ {
+ this.import_succeeded = false;
+ rcmail.set_busy(false, null, me.saving_lock);
+ rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error');
+ }
+
+ // open a dialog to select calendars for export
+ this.export_events = function(calendar)
+ {
+ // close show dialog first
+ var $dialog = $("#eventsexport"),
+ form = rcmail.gui_objects.exportform;
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (calendar)
+ $('#event-export-calendar').val(calendar.id);
+
+ $('#event-export-range').change(function(e){
+ var custom = $('option:selected', this).val() == 'custom',
+ input = $('#event-export-startdate')
+ input.parent()[(custom?'show':'hide')]();
+ if (custom)
+ input.select();
+ })
+
+ var buttons = {};
+ buttons[rcmail.gettext('export', 'calendar')] = function() {
+ if (form) {
+ var start = 0, range = $('#event-export-range option:selected', this).val(),
+ source = $('#event-export-calendar option:selected').val(),
+ attachmt = $('#event-export-attachments').get(0).checked;
+
+ if (range == 'custom')
+ start = date2unixtime(parse_datetime('00:00', $('#event-export-startdate').val()));
+ else if (range > 0)
+ start = 'today -' + range + ' months';
+
+ rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 });
+ }
+ };
+
+ buttons[rcmail.gettext('cancel', 'calendar')] = function() {
+ $dialog.dialog("close");
+ };
+
+ // open jquery UI dialog
+ $dialog.dialog({
+ modal: true,
+ resizable: false,
+ closeOnEscape: false,
+ title: rcmail.gettext('exporttitle', 'calendar'),
+ open: function() {
+ $dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
+ },
+ close: function() {
+ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
+ $dialog.dialog("destroy").hide();
+ },
+ buttons: buttons,
+ width: 520
+ }).show();
+
+ };
+
+ // download the selected event as iCal
+ this.event_download = function(event)
+ {
+ if (event && event.id) {
+ rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 });
+ }
+ };
+
+ // open the message compose step with a calendar_event parameter referencing the selected event.
+ // the server-side plugin hook will pick that up and attach the event to the message.
+ this.event_sendbymail = function(event, e)
+ {
+ if (event && event.id) {
+ rcmail.command('compose', { _calendar_event:event._id }, e ? e.target : null, e);
+ }
+ };
+
+ // show URL of the given calendar in a dialog box
+ this.showurl = function(calendar)
+ {
+ var $dialog = $('#calendarurlbox');
+
+ if ($dialog.is(':ui-dialog'))
+ $dialog.dialog('close');
+
+ if (calendar.feedurl) {
+ if (calendar.caldavurl) {
+ $('#caldavurl').val(calendar.caldavurl);
+ $('#calendarcaldavurl').show();
+ }
+ else {
+ $('#calendarcaldavurl').hide();
+ }
+
+ $dialog.dialog({
+ resizable: true,
+ closeOnEscape: true,
+ title: rcmail.gettext('showurl', 'calendar'),
+ close: function() {
+ $dialog.dialog("destroy").hide();
+ },
+ width: 520
+ }).show();
+
+ $('#calfeedurl').val(calendar.feedurl).select();
+ }
+ };
+
+ // refresh the calendar view after saving event data
+ this.refresh = function(p)
+ {
+ var source = me.calendars[p.source];
+
+ // helper function to update the given fullcalendar view
+ function update_view(view, event, source) {
+ var existing = view.fullCalendar('clientEvents', event._id);
+ if (existing.length) {
+ $.extend(existing[0], event);
+ view.fullCalendar('updateEvent', existing[0]);
+ // remove old recurrence instances
+ if (event.recurrence && !event.recurrence_id)
+ view.fullCalendar('removeEvents', function(e){ return e._id.indexOf(event._id+'-') == 0; });
+ }
+ else {
+ event.source = source; // link with source
+ view.fullCalendar('renderEvent', event);
+ }
+ }
+
+ // remove temp events
+ fc.fullCalendar('removeEvents', function(e){ return e.temp; });
+
+ if (source && (p.refetch || (p.update && !source.active))) {
+ // activate event source if new event was added to an invisible calendar
+ if (this.quickview_active) {
+ // map source to the quickview_sources equivalent
+ $.each(this.quickview_sources, function(src) {
+ if (src.id == source.id) {
+ source = src;
+ return false;
+ }
+ });
+ fc.fullCalendar('refetchEvents', source, true);
+ }
+ else if (!source.active) {
+ source.active = true;
+ fc.fullCalendar('addEventSource', source);
+ $('#rcmlical' + source.id + ' input').prop('checked', true);
+ }
+ else
+ fc.fullCalendar('refetchEvents', source, true);
+
+ fetch_counts();
+ }
+ // add/update single event object
+ else if (source && p.update) {
+ var event = p.update;
+ event.temp = false;
+ event.editable = 0;
+
+ // update fish-eye view
+ if (this.fisheye_date)
+ update_view($('#fish-eye-view'), event, source);
+
+ // update main view
+ event.editable = source.editable;
+ update_view(fc, event, source);
+
+ // update the currently displayed event dialog
+ if ($('#eventshow').is(':visible') && me.selected_event && me.selected_event.id == event.id)
+ event_show_dialog(event)
+ }
+ // refetch all calendars
+ else if (p.refetch) {
+ fc.fullCalendar('refetchEvents', undefined, true);
+ fetch_counts();
+ }
+ };
+
+ // modify query parameters for refresh requests
+ this.before_refresh = function(query)
+ {
+ var view = fc.fullCalendar('getView');
+
+ query.start = date2unixtime(view.visStart);
+ query.end = date2unixtime(view.visEnd);
+
+ if (this.search_query)
+ query.q = this.search_query;
+
+ return query;
+ };
+
+ // callback from server providing event counts
+ this.update_counts = function(p)
+ {
+ $.each(p.counts, function(cal, count) {
+ var li = calendars_list.get_item(cal),
+ bubble = $(li).children('.calendar').find('span.count');
+
+ if (!bubble.length && count > 0) {
+ bubble = $('<span>')
+ .addClass('count')
+ .appendTo($(li).children('.calendar').first())
+ }
+
+ if (count > 0) {
+ bubble.text(count).show();
+ }
+ else {
+ bubble.text('').hide();
+ }
+ });
+ };
+
+ // callback after an iTip message event was imported
+ this.itip_message_processed = function(data)
+ {
+ // remove temporary iTip source
+ fc.fullCalendar('removeEventSource', this.calendars['--invitation--itip']);
+
+ $('#eventshow:ui-dialog').dialog('close');
+ this.selected_event = null;
+
+ // refresh destination calendar source
+ this.refresh({ source:data.calendar, refetch:true });
+
+ this.unlock_saving();
+
+ // process 'after_action' in mail task
+ if (window.opener && window.opener.rcube_libcalendaring)
+ window.opener.rcube_libcalendaring.itip_message_processed(data);
+ };
+
+ // reload the calendar view by keeping the current date/view selection
+ this.reload_view = function()
+ {
+ var query = { view: fc.fullCalendar('getView').name },
+ date = fc.fullCalendar('getDate');
+ if (date)
+ query.date = date2unixtime(date);
+ rcmail.redirect(rcmail.url('', query));
+ }
+
+ // update browser location to remember current view
+ this.update_state = function()
+ {
+ var query = { view: current_view },
+ date = fc.fullCalendar('getDate');
+ if (date)
+ query.date = date2unixtime(date);
+
+ if (window.history.replaceState)
+ window.history.replaceState({}, document.title, rcmail.url('', query).replace('&_action=', ''));
+ };
+
+ this.resource_search = resource_search;
+ this.reset_resource_search = reset_resource_search;
+ this.add_resource2event = add_resource2event;
+ this.resource_data_load = resource_data_load;
+ this.resource_owner_load = resource_owner_load;
+
+
+ /*** event searching ***/
+
+ // execute search
+ this.quicksearch = function()
+ {
+ if (rcmail.gui_objects.qsearchbox) {
+ var q = rcmail.gui_objects.qsearchbox.value;
+ if (q != '') {
+ var id = 'search-'+q;
+ var sources = [];
+
+ if (me.quickview_active)
+ reset_quickview();
+
+ if (this._search_message)
+ rcmail.hide_message(this._search_message);
+
+ for (var sid in this.calendars) {
+ if (this.calendars[sid]) {
+ this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '') + '&q=' + urlencode(q);
+ sources.push(sid);
+ }
+ }
+ id += '@'+sources.join(',');
+
+ // ignore if query didn't change
+ if (this.search_request == id) {
+ return;
+ }
+ // remember current view
+ else if (!this.search_request) {
+ this.default_view = fc.fullCalendar('getView').name;
+ }
+
+ this.search_request = id;
+ this.search_query = q;
+
+ // change to list view
+ fc.fullCalendar('option', 'listSections', 'month')
+ .fullCalendar('option', 'listRange', Math.max(60, settings['agenda_range']))
+ .fullCalendar('changeView', 'table');
+
+ update_agenda_toolbar();
+
+ // refetch events with new url (if not already triggered by changeView)
+ if (!this.is_loading)
+ fc.fullCalendar('refetchEvents');
+ }
+ else // empty search input equals reset
+ this.reset_quicksearch();
+ }
+ };
+
+ // reset search and get back to normal event listing
+ this.reset_quicksearch = function()
+ {
+ $(rcmail.gui_objects.qsearchbox).val('');
+
+ if (this._search_message)
+ rcmail.hide_message(this._search_message);
+
+ if (this.search_request) {
+ // hide bottom links of agenda view
+ fc.find('.fc-list-content > .fc-listappend').hide();
+
+ // restore original event sources and view mode from fullcalendar
+ fc.fullCalendar('option', 'listSections', settings['agenda_sections'])
+ .fullCalendar('option', 'listRange', settings['agenda_range']);
+
+ update_agenda_toolbar();
+
+ for (var sid in this.calendars) {
+ if (this.calendars[sid])
+ this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '');
+ }
+ if (this.default_view)
+ fc.fullCalendar('changeView', this.default_view);
+
+ if (!this.is_loading)
+ fc.fullCalendar('refetchEvents');
+
+ this.search_request = this.search_query = null;
+ }
+ };
+
+ // callback if all sources have been fetched from server
+ this.events_loaded = function(count)
+ {
+ var addlinks, append = '';
+
+ // enhance list view when searching
+ if (this.search_request) {
+ if (!count) {
+ this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice');
+ append = '<div class="message">' + rcmail.gettext('searchnoresults', 'calendar') + '</div>';
+ }
+ append += '<div class="fc-bottomlinks formlinks"></div>';
+ addlinks = true;
+ }
+
+ if (fc.fullCalendar('getView').name == 'table') {
+ var container = fc.find('.fc-list-content > .fc-listappend');
+ if (append) {
+ if (!container.length)
+ container = $('<div class="fc-listappend"></div>').appendTo(fc.find('.fc-list-content'));
+ container.html(append).show();
+ }
+ else if (container.length)
+ container.hide();
+
+ // add links to adjust search date range
+ if (addlinks) {
+ var lc = container.find('.fc-bottomlinks');
+ $('<a>').attr('href', '#').html(rcmail.gettext('searchearlierdates', 'calendar')).appendTo(lc).click(function(){
+ fc.fullCalendar('incrementDate', 0, -1, 0);
+ });
+ lc.append(" ");
+ $('<a>').attr('href', '#').html(rcmail.gettext('searchlaterdates', 'calendar')).appendTo(lc).click(function(){
+ var range = fc.fullCalendar('option', 'listRange');
+ if (range < 90) {
+ fc.fullCalendar('option', 'listRange', fc.fullCalendar('option', 'listRange') + 30).fullCalendar('render');
+ update_agenda_toolbar();
+ }
+ else
+ fc.fullCalendar('incrementDate', 0, 1, 0);
+ });
+ }
+ }
+
+ if (this.fisheye_date)
+ this.fisheye_view(this.fisheye_date);
+ };
+
+ // resize and reposition (center) the dialog window
+ this.dialog_resize = function(id, height, width)
+ {
+ var win = $(window), w = win.width(), h = win.height();
+ $(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) })
+ .dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?)
+ };
+
+ // adjust calendar view size
+ this.view_resize = function()
+ {
+ var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').outerHeight() : 0;
+ fc.fullCalendar('option', 'height', $('#calendar').height() - footer);
+ };
+
+ // mark the given calendar folder as selected
+ this.select_calendar = function(id, nolistupdate)
+ {
+ if (!nolistupdate)
+ calendars_list.select(id);
+
+ // trigger event hook
+ rcmail.triggerEvent('selectfolder', { folder:id, prefix:'rcmlical' });
+
+ this.selected_calendar = id;
+ };
+
+ // register the given calendar to the current view
+ var add_calendar_source = function(cal)
+ {
+ var color, brightness, select, id = cal.id;
+
+ me.calendars[id] = $.extend({
+ url: rcmail.url('calendar/load_events', { source: id, driver: cal.driver }),
+ editable: !cal.readonly,
+ className: 'fc-event-cal-'+id,
+ id: id
+ }, cal);
+
+ // choose black text color when background is bright, white otherwise
+ if (color = settings.event_coloring % 2 ? '' : '#' + cal.color) {
+ if (/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.test(color)) {
+ // use information about brightness calculation found at
+ // http://javascriptrules.com/2009/08/05/css-color-brightness-contrast-using-javascript/
+ brightness = (parseInt(RegExp.$1, 16) * 299 + parseInt(RegExp.$2, 16) * 587 + parseInt(RegExp.$3, 16) * 114) / 1000;
+ if (brightness > 125)
+ me.calendars[id].textColor = 'black';
+ }
+
+ me.calendars[id].color = color;
+ }
+
+ if (fc && (cal.active || cal.subscribed)) {
+ if (cal.active)
+ fc.fullCalendar('addEventSource', me.calendars[id]);
+
+ var submit = { id: id, active: cal.active ? 1 : 0 };
+ if (cal.subscribed !== undefined)
+ submit.permanent = cal.subscribed ? 1 : 0;
+ rcmail.http_post('calendar', { action:'subscribe', c:submit });
+ }
+
+ // insert to #calendar-select options if writeable
+ select = $('#edit-calendar');
+ if (fc && has_permission(cal, 'i') && select.length && !select.find('option[value="'+id+'"]').length) {
+ $('<option>').attr('value', id).html(cal.name).appendTo(select);
+ }
+ }
+
+ // fetch counts for some calendars from the server
+ var fetch_counts = function()
+ {
+ if (count_sources.length) {
+ setTimeout(function() {
+ rcmail.http_request('calendar/count', { source:count_sources });
+ }, 500);
+ }
+ };
+
+
+ /*** startup code ***/
+
+ // create list of event sources AKA calendars
+ var id, cal, active, event_sources = [];
+ for (id in rcmail.env.calendars) {
+ cal = rcmail.env.calendars[id];
+ active = cal.active || false;
+ add_calendar_source(cal);
+
+ // check active calendars
+ $('#rcmlical'+id+' > .calendar input').prop('checked', active);
+
+ if (active) {
+ event_sources.push(this.calendars[id]);
+ }
+ if (cal.counts) {
+ count_sources.push(id);
+ }
+
+ if (cal.editable && !this.selected_calendar) {
+ this.selected_calendar = id;
+ rcmail.enable_command('addevent', true);
+ }
+ }
+
+ // initialize treelist widget that controls the calendars list
+ var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+ calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
+ id_prefix: 'rcmlical',
+ selectable: true,
+ save_state: true,
+ keyboard: false,
+ searchbox: '#calendarlistsearch',
+ search_action: 'calendar/calendar',
+ search_sources: [ 'folders', 'users' ],
+ search_title: rcmail.gettext('calsearchresults','calendar')
+ });
+ calendars_list.addEventListener('select', function(node) {
+ if (node && node.id && me.calendars[node.id]) {
+ me.select_calendar(node.id, true);
+ rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
+ rcmail.enable_command('calendar-delete', me.calendars[node.id] && (me.calendars[node.id].deletable || !me.calendars[node.id].readonly));
+ rcmail.enable_command('calendar-remove', me.calendars[node.id] && me.calendars[node.id].removable);
+ }
+ });
+ calendars_list.addEventListener('insert-item', function(p) {
+ var cal = p.data;
+ if (cal && cal.id) {
+ add_calendar_source(cal);
+
+ // add css classes related to this calendar to document
+ if (cal.css) {
+ $('<style type="text/css"></style>')
+ .html(cal.css)
+ .appendTo('head');
+ }
+ }
+ });
+ calendars_list.addEventListener('subscribe', function(p) {
+ var cal;
+ if ((cal = me.calendars[p.id])) {
+ cal.subscribed = p.subscribed || false;
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
+ }
+ });
+ calendars_list.addEventListener('remove', function(p) {
+ if (me.calendars[p.id] && me.calendars[p.id].removable) {
+ me.calendar_remove(me.calendars[p.id]);
+ }
+ });
+ calendars_list.addEventListener('search-complete', function(data) {
+ if (data.length)
+ rcmail.display_message(rcmail.gettext('nrcalendarsfound','calendar').replace('$nr', data.length), 'voice');
+ else
+ rcmail.display_message(rcmail.gettext('nocalendarsfound','calendar'), 'info');
+ });
+ calendars_list.addEventListener('click-item', function(event) {
+ // handle clicks on quickview icon: temprarily add this source and open in quickview
+ if ($(event.target).hasClass('quickview') && event.data) {
+ if (!me.calendars[event.data.id]) {
+ event.data.readonly = true;
+ event.data.active = false;
+ event.data.subscribed = false;
+ add_calendar_source(event.data);
+ }
+ me.quickview(event.data.id, event.shiftKey || event.metaKey || event.ctrlKey);
+ return false;
+ }
+ });
+
+ // init (delegate) event handler on calendar list checkboxes
+ $(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e) {
+ e.stopPropagation();
+
+ if (me.quickview_active) {
+ this.checked = !this.checked;
+ return false;
+ }
+
+ var id = this.value;
+ if (me.calendars[id]) { // add or remove event source on click
+ var action;
+ if (this.checked) {
+ action = 'addEventSource';
+ me.calendars[id].active = true;
+ }
+ else {
+ action = 'removeEventSource';
+ me.calendars[id].active = false;
+ }
+
+ // adjust checked state of original list item
+ if (calendars_list.is_search()) {
+ calendars_list.container.find('input[value="'+id+'"]').prop('checked', this.checked);
+ }
+
+ // add/remove event source
+ fc.fullCalendar(action, me.calendars[id]);
+ rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
+ }
+ })
+ .on('keypress', 'input[type=checkbox]', function(e) {
+ // select calendar on <Enter>
+ if (e.keyCode == 13) {
+ calendars_list.select(this.value);
+ return rcube_event.cancel(e);
+ }
+ })
+ // init (delegate) event handler on quickview links
+ .on('click', 'a.quickview', function(e) {
+ var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
+
+ if (calendars_list.is_search())
+ id = id.replace(/--xsR$/, '');
+
+ if (me.calendars[id])
+ me.quickview(id, e.shiftKey || e.metaKey || e.ctrlKey);
+
+ if (!rcube_event.is_keyboard(e) && this.blur)
+ this.blur();
+
+ e.stopPropagation();
+ return false;
+ });
+
+ // register dbl-click handler to open calendar edit dialog
+ $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){
+ var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
+ me.calendar_edit_dialog(me.calendars[id]);
+ });
+
+ // select default calendar
+ if (settings.default_calendar && this.calendars[settings.default_calendar] && this.calendars[settings.default_calendar].editable)
+ this.selected_calendar = settings.default_calendar;
+
+ if (this.selected_calendar)
+ this.select_calendar(this.selected_calendar);
+
+ var viewdate = new Date();
+ if (rcmail.env.date)
+ viewdate.setTime(fromunixtime(rcmail.env.date));
+
+ // add source with iTip event data for rendering
+ if (rcmail.env.itip_events && rcmail.env.itip_events.length) {
+ me.calendars['--invitation--itip'] = {
+ events: rcmail.env.itip_events,
+ className: 'fc-event-cal---invitation--itip',
+ color: '#fff',
+ textColor: '#333',
+ editable: false,
+ rights: 'lrs',
+ attendees: true
+ };
+ event_sources.push(me.calendars['--invitation--itip']);
+ }
+
+ // initalize the fullCalendar plugin
+ var fc = $('#calendar').fullCalendar($.extend({}, fullcalendar_defaults, {
+ header: {
+ right: 'prev,next today',
+ center: 'title',
+ left: 'agendaDay,agendaWeek,month,table'
+ },
+ date: viewdate.getDate(),
+ month: viewdate.getMonth(),
+ year: viewdate.getFullYear(),
+ height: $('#calendar').height(),
+ eventSources: event_sources,
+ selectable: true,
+ selectHelper: false,
+ loading: function(isLoading) {
+ me.is_loading = isLoading;
+ this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading);
+ // trigger callback
+ if (!isLoading)
+ me.events_loaded($(this).fullCalendar('clientEvents').length);
+ },
+ // callback for date range selection
+ select: function(start, end, allDay, e, view) {
+ var range_select = (!allDay || start.getDate() != end.getDate())
+ if (dialog_check(e) && range_select)
+ event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar });
+ if (range_select || ignore_click)
+ view.calendar.unselect();
+ },
+ // callback for clicks in all-day box
+ dayClick: function(date, allDay, e, view) {
+ var now = new Date().getTime();
+ if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day
+ var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
+ return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar });
+ }
+
+ if (!ignore_click) {
+ view.calendar.gotoDate(date);
+ if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth())
+ view.calendar.select(date, date, allDay);
+ }
+ day_clicked = date.getTime();
+ day_clicked_ts = now;
+ },
+ // callback when an event was dragged and finally dropped
+ eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
+ if (event.end == null || event.end.getTime() < event.start.getTime()) {
+ event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS));
+ }
+ // moved to all-day section: set times to 12:00 - 13:00
+ if (allDay && !event.allDay) {
+ event.start.setHours(12);
+ event.start.setMinutes(0);
+ event.start.setSeconds(0);
+ event.end.setHours(13);
+ event.end.setMinutes(0);
+ event.end.setSeconds(0);
+ }
+ // moved from all-day section: set times to working hours
+ else if (event.allDay && !allDay) {
+ var newstart = event.start.getTime();
+ revertFunc(); // revert to get original duration
+ var numdays = Math.max(1, Math.round((event.end.getTime() - event.start.getTime()) / DAY_MS)) - 1;
+ event.start = new Date(newstart);
+ event.end = new Date(newstart + numdays * DAY_MS);
+ event.end.setHours(settings['work_end'] || 18);
+ event.end.setMinutes(0);
+
+ if (event.end.getTime() < event.start.getTime())
+ event.end = new Date(newstart + HOUR_MS);
+ }
+
+ // send move request to server
+ var data = {
+ id: event.id,
+ calendar: event.calendar,
+ start: date2servertime(event.start),
+ end: date2servertime(event.end),
+ allday: allDay?1:0
+ };
+ update_event_confirm('move', event, data);
+ },
+ // callback for event resizing
+ eventResize: function(event, delta) {
+ // sanitize event dates
+ if (event.allDay)
+ event.start.setHours(12);
+ if (!event.end || event.end.getTime() < event.start.getTime())
+ event.end = new Date(event.start.getTime() + HOUR_MS);
+
+ // send resize request to server
+ var data = {
+ id: event.id,
+ calendar: event.calendar,
+ start: date2servertime(event.start),
+ end: date2servertime(event.end)
+ };
+ update_event_confirm('resize', event, data);
+ },
+ viewDisplay: function(view) {
+ $('#agendaoptions')[view.name == 'table' ? 'show' : 'hide']();
+ if (minical) {
+ window.setTimeout(function(){ minical.datepicker('setDate', fc.fullCalendar('getDate')); }, exec_deferred);
+ if (view.name != current_view)
+ me.view_resize();
+ current_view = view.name;
+ me.update_state();
+ }
+ },
+ viewRender: function(view) {
+ if (fc && view.name == 'month')
+ fc.fullCalendar('option', 'maxHeight', Math.floor((view.element.parent().height()-18) / 6) - 35);
+ }
+ }));
+
+ // format time string
+ var formattime = function(hour, minutes, start) {
+ var time, diff, unit, duration = '', d = new Date();
+ d.setHours(hour);
+ d.setMinutes(minutes);
+ time = $.fullCalendar.formatDate(d, settings['time_format']);
+ if (start) {
+ diff = Math.floor((d.getTime() - start.getTime()) / 60000);
+ if (diff > 0) {
+ unit = 'm';
+ if (diff >= 60) {
+ unit = 'h';
+ diff = Math.round(diff / 3) / 20;
+ }
+ duration = ' (' + diff + unit + ')';
+ }
+ }
+ return [time, duration];
+ };
+
+ var autocomplete_times = function(p, callback) {
+ /* Time completions */
+ var result = [];
+ var now = new Date();
+ var st, start = (String(this.element.attr('id')).indexOf('endtime') > 0
+ && (st = $('#edit-starttime').val())
+ && $('#edit-startdate').val() == $('#edit-enddate').val())
+ ? parse_datetime(st, '') : null;
+ var full = p.term - 1 > 0 || p.term.length > 1;
+ var hours = start ? start.getHours() :
+ (full ? parse_datetime(p.term, '') : now).getHours();
+ var step = 15;
+ var minutes = hours * 60 + (full ? 0 : now.getMinutes());
+ var min = Math.ceil(minutes / step) * step % 60;
+ var hour = Math.floor(Math.ceil(minutes / step) * step / 60);
+ // list hours from 0:00 till now
+ for (var h = start ? start.getHours() : 0; h < hours; h++)
+ result.push(formattime(h, 0, start));
+ // list 15min steps for the next two hours
+ for (; h < hour + 2 && h < 24; h++) {
+ while (min < 60) {
+ result.push(formattime(h, min, start));
+ min += step;
+ }
+ min = 0;
+ }
+ // list the remaining hours till 23:00
+ while (h < 24)
+ result.push(formattime((h++), 0, start));
+
+ return callback(result);
+ };
+
+ var autocomplete_open = function(event, ui) {
+ // scroll to current time
+ var $this = $(this);
+ var widget = $this.autocomplete('widget');
+ var menu = $this.data('ui-autocomplete').menu;
+ var amregex = /^(.+)(a[.m]*)/i;
+ var pmregex = /^(.+)(a[.m]*)/i;
+ var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
+ var li, html;
+ widget.css('width', '10em');
+ widget.children().each(function(){
+ li = $(this);
+ html = li.children().first().html().replace(/\s+\(.+\)$/, '').replace(amregex, '0:$1').replace(pmregex, '1:$1');
+ if (html.indexOf(val) == 0)
+ menu._scrollIntoView(li);
+ });
+ };
+
+ // if start date is changed, shift end date according to initial duration
+ var shift_enddate = function(dateText) {
+ var newstart = parse_datetime('0', dateText);
+ var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
+ $('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
+ event_times_changed();
+ };
+
+ // Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
+ // Uses the default $.datepicker.iso8601Week() function but takes firstDay setting into account.
+ // This is a temporary fix until http://bugs.jqueryui.com/ticket/8420 is resolved.
+ var iso8601Week = datepicker_settings.calculateWeek = function(date) {
+ var mondayOffset = Math.abs(1 - datepicker_settings.firstDay);
+ return $.datepicker.iso8601Week(new Date(date.getTime() + mondayOffset * 86400000));
+ };
+
+ var minical;
+ var init_calendar_ui = function()
+ {
+ // initialize small calendar widget using jQuery UI datepicker
+ minical = $('#datepicker').datepicker($.extend(datepicker_settings, {
+ inline: true,
+ showWeek: true,
+ changeMonth: true,
+ changeYear: true,
+ onSelect: function(dateText, inst) {
+ ignore_click = true;
+ var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText);
+ fc.fullCalendar('gotoDate', d).fullCalendar('select', d, d, true);
+ },
+ onChangeMonthYear: function(year, month, inst) {
+ minical.data('year', year).data('month', month);
+ },
+ beforeShowDay: function(date) {
+ var view = fc.fullCalendar('getView');
+ var active = view.visStart && date.getTime() >= view.visStart.getTime() && date.getTime() < view.visEnd.getTime();
+ return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
+ }
+ })) // set event handler for clicks on calendar week cell of the datepicker widget
+ .on('click', 'td.ui-datepicker-week-col', function(e) {
+ var cell = $(e.target);
+ if (e.target.tagName == 'TD') {
+ var base_date = minical.datepicker('getDate');
+ if (minical.data('month'))
+ base_date.setMonth(minical.data('month')-1);
+ if (minical.data('year'))
+ base_date.setYear(minical.data('year'));
+ base_date.setHours(12);
+ base_date.setDate(base_date.getDate() - ((base_date.getDay() + 6) % 7) + datepicker_settings.firstDay);
+ var base_kw = iso8601Week(base_date),
+ target_kw = parseInt(cell.html()),
+ wdiff = target_kw - base_kw;
+ if (wdiff > 10) // year jump
+ base_date.setYear(base_date.getFullYear() - 1);
+ else if (wdiff < -10)
+ base_date.setYear(base_date.getFullYear() + 1);
+ // select monday of the chosen calendar week
+ var day_off = base_date.getDay() - datepicker_settings.firstDay,
+ date = new Date(base_date.getTime() - day_off * DAY_MS + wdiff * 7 * DAY_MS);
+ fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
+ minical.datepicker('setDate', date);
+ }
+ });
+
+ minical.find('.ui-datepicker-inline').attr('aria-labelledby', 'aria-label-minical');
+
+ if (rcmail.env.date) {
+ var viewdate = new Date();
+ viewdate.setTime(fromunixtime(rcmail.env.date));
+ minical.datepicker('setDate', viewdate);
+ }
+
+ // init event dialog
+ $('#eventtabs').tabs({
+ activate: function(event, ui) {
+ if (ui.newPanel.selector == '#event-panel-attendees' || ui.newPanel.selector == '#event-panel-resources') {
+ var tab = ui.newPanel.selector == '#event-panel-resources' ? 'resource' : 'attendee';
+ if (!rcube_event.is_keyboard(event))
+ $('#edit-'+tab+'-name').select();
+ // update free-busy status if needed
+ if (freebusy_ui.needsupdate && me.selected_event)
+ update_freebusy_status(me.selected_event);
+ // add current user as organizer if non added yet
+ if (!event_attendees.length) {
+ add_attendee($.extend({ role:'ORGANIZER' }, settings.identity));
+ $('#edit-attendees-form .attendees-invitebox').show();
+ }
+ }
+ // reset autocompletion on tab change (#3389)
+ if (ui.oldPanel.selector == '#event-panel-attendees' || ui.oldPanel.selector == '#event-panel-resources') {
+ rcmail.ksearch_blur();
+ }
+ }
+ });
+ $('#edit-enddate').datepicker(datepicker_settings);
+ $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
+ $('#edit-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed);
+ $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); });
+
+ // configure drop-down menu on time input fields based on jquery UI autocomplete
+ $('#edit-starttime, #edit-endtime, #eventedit input.edit-alarm-time')
+ .attr('autocomplete', "off")
+ .autocomplete({
+ delay: 100,
+ minLength: 1,
+ appendTo: '#eventedit',
+ source: autocomplete_times,
+ open: autocomplete_open,
+ change: event_times_changed,
+ select: function(event, ui) {
+ $(this).val(ui.item[0]).change();
+ return false;
+ }
+ })
+ .click(function() { // show drop-down upon clicks
+ $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
+ }).each(function(){
+ $(this).data('ui-autocomplete')._renderItem = function(ul, item) {
+ return $('<li>')
+ .data('ui-autocomplete-item', item)
+ .append('<a>' + item[0] + item[1] + '</a>')
+ .appendTo(ul);
+ };
+ });
+
+ // adjust end time when changing start
+ $('#edit-starttime').change(function(e) {
+ var dstart = $('#edit-startdate'),
+ newstart = parse_datetime(this.value, dstart.val()),
+ newend = new Date(newstart.getTime() + dstart.data('duration') * 1000);
+ $('#edit-endtime').val($.fullCalendar.formatDate(newend, me.settings['time_format']));
+ $('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
+ event_times_changed();
+ });
+
+ // register events on alarms and recurrence fields
+ me.init_alarms_edit('#edit-alarms');
+ me.init_recurrence_edit('#eventedit');
+
+ // reload free-busy status when changing the organizer identity
+ $('#eventedit').on('change', '#edit-identities-list', function(e) {
+ var email = settings.identities[$(this).val()],
+ icon = $(this).closest('tr').find('img.availabilityicon');
+
+ if (email && icon.length) {
+ icon.attr('data-email', email);
+ check_freebusy_status(icon, email, me.selected_event);
+ }
+ });
+
+ $('#event-export-startdate').datepicker(datepicker_settings);
+
+ // init attendees autocompletion
+ var ac_props;
+ // parallel autocompletion
+ if (rcmail.env.autocomplete_threads > 0) {
+ ac_props = {
+ threads: rcmail.env.autocomplete_threads,
+ sources: rcmail.env.autocomplete_sources
+ };
+ }
+ rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
+ rcmail.addEventListener('autocomplete_insert', function(e) {
+ var success = false;
+ if (e.field.name == 'participant') {
+ success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:(e.data && e.data.type == 'group' ? 'GROUP' : 'INDIVIDUAL') });
+ }
+ else if (e.field.name == 'resource' && e.data && e.data.email) {
+ success = add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+ }
+ if (e.field && success) {
+ e.field.value = '';
+ }
+ });
+
+ $('#edit-attendee-add').click(function(){
+ var input = $('#edit-attendee-name');
+ rcmail.ksearch_blur();
+ if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
+ input.val('');
+ }
+ });
+
+ rcmail.init_address_input_events($('#edit-resource-name'), { action:'calendar/resources-autocomplete' });
+
+ $('#edit-resource-add').click(function(){
+ var input = $('#edit-resource-name');
+ rcmail.ksearch_blur();
+ if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })) {
+ input.val('');
+ }
+ });
+
+ $('#edit-resource-find').click(function(){
+ event_resources_dialog();
+ return false;
+ });
+
+ // handle change of "send invitations" checkbox
+ $('#edit-attendees-invite').change(function() {
+ $('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked);
+ // hide/show comment field
+ $('#eventedit .attendees-commentbox')[this.checked ? 'show' : 'hide']();
+ });
+
+ // delegate change event to "send invitations" checkbox
+ $('#edit-attendees-donotify').change(function() {
+ $('#edit-attendees-invite').click();
+ return false;
+ });
+
+ $('#edit-attendee-schedule').click(function(){
+ event_freebusy_dialog();
+ });
+
+ $('#shedule-freebusy-prev').html(bw.ie6 ? '&lt;&lt;' : '&#9668;').button().click(function(){ render_freebusy_grid(-1); });
+ $('#shedule-freebusy-next').html(bw.ie6 ? '&gt;&gt;' : '&#9658;').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
+
+ $('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); });
+ $('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); });
+
+ $('#schedule-freebusy-workinghours').click(function(){
+ freebusy_ui.workinhoursonly = this.checked;
+ $('#workinghourscss').remove();
+ if (this.checked)
+ $('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');
+ });
+
+ $('#event-rsvp input.button').click(function(e) {
+ event_rsvp(this)
+ });
+
+ $('#eventedit input.edit-recurring-savemode').change(function(e) {
+ var sel = $('input.edit-recurring-savemode:checked').val(),
+ disabled = sel == 'current' || sel == 'future';
+ $('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', disabled);
+ $('#event-panel-recurrence, #event-panel-attachments')[(disabled?'addClass':'removeClass')]('disabled');
+ })
+
+ $('#eventshow .changersvp').click(function(e) {
+ var d = $('#eventshow'),
+ h = -$(this).closest('.event-line').toggle().height();
+ $('#event-rsvp').slideDown(300, function() {
+ h += $(this).height();
+ me.dialog_resize(d.get(0), d.height() + h, d.outerWidth() - 50);
+ });
+ return false;
+ })
+
+ // register click handler for message links
+ $('#edit-event-links, #event-links').on('click', 'li a.messagelink', function(e) {
+ rcmail.open_window(this.href);
+ if (!rcube_event.is_keyboard(e) && this.blur)
+ this.blur();
+ return false;
+ });
+
+ // register click handler for message delete buttons
+ $('#edit-event-links').on('click', 'li a.delete', function(e) {
+ remove_link(e.target);
+ return false;
+ });
+
+ $('#agenda-listrange').change(function(e){
+ settings['agenda_range'] = parseInt($(this).val());
+ fc.fullCalendar('option', 'listRange', settings['agenda_range']).fullCalendar('render');
+ // TODO: save new settings in prefs
+ }).val(settings['agenda_range']);
+
+ $('#agenda-listsections').change(function(e){
+ settings['agenda_sections'] = $(this).val();
+ fc.fullCalendar('option', 'listSections', settings['agenda_sections']).fullCalendar('render');
+ // TODO: save new settings in prefs
+ }).val(fc.fullCalendar('option', 'listSections'));
+
+ // hide event dialog when clicking somewhere into document
+ $(document).bind('mousedown', dialog_check);
+
+ rcmail.set_busy(false, 'loading', ui_loading);
+ }
+
+ // initialize more UI elements (deferred)
+ window.setTimeout(init_calendar_ui, exec_deferred);
+
+ // fetch counts for some calendars
+ fetch_counts();
+
+ // add proprietary css styles if not IE
+ if (!bw.ie)
+ $('div.fc-content').addClass('rcube-fc-content');
+
+ // IE supresses 2nd click event when double-clicking
+ if (bw.ie && bw.vendver < 9) {
+ $('div.fc-content').bind('dblclick', function(e){
+ if (!$(this).hasClass('fc-widget-header') && fc.fullCalendar('getView').name != 'table') {
+ var date = fc.fullCalendar('getDate');
+ var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
+ event_edit_dialog('new', { start:date, end:enddate, allDay:true, calendar:me.selected_calendar });
+ }
+ });
+ }
+} // end rcube_calendar class
+
+
+/* calendar plugin initialization */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+ // configure toolbar buttons
+ rcmail.register_command('addevent', function(){ cal.add_event(); }, true);
+ rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
+
+ // configure list operations
+ rcmail.register_command('calendar-create', function(props){ cal.calendar_edit_dialog($.extend($.parseJSON(props), { name:'', color:'cc0000', editable:true, showalarms:true })); }, true);
+ rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('calendar-delete', function(){ cal.calendar_delete(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true);
+ rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
+ rcmail.register_command('event-download', function(){ cal.event_download(cal.selected_event); }, true);
+ rcmail.register_command('event-sendbymail', function(p, obj, e){ cal.event_sendbymail(cal.selected_event, e); }, true);
+ rcmail.register_command('event-history', function(p, obj, e){ cal.event_history_dialog(cal.selected_event); }, false);
+
+ // search and export events
+ rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
+ rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
+ rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
+
+ // resource invitation dialog
+ rcmail.register_command('search-resource', function(){ cal.resource_search(); }, true);
+ rcmail.register_command('reset-resource-search', function(){ cal.reset_resource_search(); }, true);
+ rcmail.register_command('add-resource', function(){ cal.add_resource2event(); }, false);
+
+ // register callback commands
+ rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
+ rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
+ rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
+ rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
+ rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
+ rcmail.addEventListener('plugin.update_counts', function(p){ cal.update_counts(p); });
+ rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); });
+ rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); });
+ rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); });
+ rcmail.addEventListener('plugin.render_event_changelog', function(data){ cal.render_event_changelog(data); });
+ rcmail.addEventListener('plugin.event_show_diff', function(data){ cal.event_show_diff(data); });
+ rcmail.addEventListener('plugin.close_history_dialog', function(data){ cal.close_history_dialog(); });
+ rcmail.addEventListener('plugin.event_show_revision', function(data){ cal.event_show_dialog(data, null, true); });
+ rcmail.addEventListener('plugin.itip_message_processed', function(data){ cal.itip_message_processed(data); });
+ rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
+
+ // let's go
+ var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
+
+ $(window).resize(function(e) {
+ // check target due to bugs in jquery
+ // http://bugs.jqueryui.com/ticket/7514
+ // http://bugs.jquery.com/ticket/9841
+ if (e.target == window) {
+ cal.view_resize();
+ }
+ }).resize();
+
+ // show calendars list when ready
+ $('#calendars').css('visibility', 'inherit');
+
+ // show toolbar
+ $('#toolbar').show();
+
+});
diff --git a/calendar/composer.json b/calendar/composer.json
new file mode 100644
index 0000000..e0c9b6d
--- /dev/null
+++ b/calendar/composer.json
@@ -0,0 +1,31 @@
+{
+ "name": "kolab/calendar",
+ "type": "roundcube-plugin",
+ "description": "Calendar plugin",
+ "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
+ "license": "AGPLv3",
+ "version": "3.2.8",
+ "authors": [
+ {
+ "name": "Thomas Bruederli",
+ "email": "bruederli@kolabsys.com",
+ "role": "Lead"
+ },
+ {
+ "name": "Alensader Machniak",
+ "email": "machniak@kolabsys.com",
+ "role": "Developer"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "http://plugins.roundcube.net"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "roundcube/plugin-installer": ">=0.1.3",
+ "kolab/libcalendaring": ">=3.2.8"
+ }
+}
diff --git a/calendar/config.inc.php.dist b/calendar/config.inc.php.dist
new file mode 100644
index 0000000..7d771c3
--- /dev/null
+++ b/calendar/config.inc.php.dist
@@ -0,0 +1,198 @@
+<?php
+/*
+ +-------------------------------------------------------------------------+
+ | Configuration for the Calendar plugin |
+ | |
+ | Copyright (C) 2010, Lazlo Westerhof - Netherlands |
+ | Copyright (C) 2011-2014, Kolab Systems AG |
+ | |
+ | This program is free software: you can redistribute it and/or modify |
+ | it under the terms of the GNU Affero 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 Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public License|
+ | along with this program. If not, see <http://www.gnu.org/licenses/>. |
+ | |
+ +-------------------------------------------------------------------------+
+ | Author: Lazlo Westerhof <hello@lazlo.me> |
+ | Thomas Bruederli <bruederli@kolabsys.com> |
+ +-------------------------------------------------------------------------+
+*/
+
+// backend type (database, kolab, caldav, ical)
+$config['calendar_driver'] = array("kolab");
+$config['calendar_driver_default'] = "kolab";
+
+// default calendar view (agendaDay, agendaWeek, month)
+$config['calendar_default_view'] = "agendaWeek";
+
+// show a birthdays calendar from the user's address book(s)
+$config['calendar_contact_birthdays'] = false;
+
+// mapping of Roundcube date formats to calendar formats (long/short/agenda)
+// should be in sync with 'date_formats' in main config
+$config['calendar_date_format_sets'] = array(
+ 'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
+ 'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
+ 'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
+ 'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
+ 'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
+ 'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
+ 'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
+);
+
+// general date format (only set if different from default date format and not user configurable)
+// $config['calendar_date_format'] = "yyyy-MM-dd";
+
+// time format (only set if different from default date format)
+// $config['calendar_time_format'] = "HH:mm";
+
+// short date format (used for column titles)
+// $config['calendar_date_short'] = 'M-d';
+
+// long date format (used for calendar title)
+// $config['calendar_date_long'] = 'MMM d yyyy';
+
+// date format used for agenda view
+// $config['calendar_date_agenda'] = 'ddd MM-dd';
+
+// timeslots per hour (1, 2, 3, 4, 6)
+$config['calendar_timeslots'] = 2;
+
+// show this number of days in agenda view
+$config['calendar_agenda_range'] = 60;
+
+// first day of the week (0-6)
+$config['calendar_first_day'] = 1;
+
+// first hour of the calendar (0-23)
+$config['calendar_first_hour'] = 6;
+
+// working hours begin
+$config['calendar_work_start'] = 6;
+
+// working hours end
+$config['calendar_work_end'] = 18;
+
+// show line at current time of the day
+$config['calendar_time_indicator'] = true;
+
+// default alarm settings for new events.
+// this is only a preset when a new event dialog opens
+// possible values are <empty>, DISPLAY, EMAIL
+$config['calendar_default_alarm_type'] = '';
+
+// default alarm offset for new events.
+// use ical-style offset values like "-1H" (one hour before) or "+30M" (30 minutes after)
+$config['calendar_default_alarm_offset'] = '-15M';
+
+// how to colorize events:
+// 0: according to calendar color
+// 1: according to category color
+// 2: calendar for outer, category for inner color
+// 3: category for outer, calendar for inner color
+$config['calendar_event_coloring'] = 0;
+
+// event categories
+$config['calendar_categories'] = array(
+ 'Personal' => 'c0c0c0',
+ 'Work' => 'ff0000',
+ 'Family' => '00ff00',
+ 'Holiday' => 'ff6600',
+);
+
+// enable users to invite/edit attendees for shared events organized by others
+$config['calendar_allow_invite_shared'] = false;
+
+// allow users to accecpt iTip invitations who are no explicitly listed as attendee.
+// this can be the case if invitations are sent to mailing lists or alias email addresses.
+$config['calendar_allow_itip_uninvited'] = true;
+
+// controls the visibility/default of the checkbox controlling the sending of iTip invitations
+// 0 = hidden + disabled
+// 1 = hidden + active
+// 2 = visible + unchecked
+// 3 = visible + active
+$config['calendar_itip_send_option'] = 3;
+
+// Action taken after iTip request is handled. Possible values:
+// 0 - no action
+// 1 - move to Trash
+// 2 - delete the message
+// 3 - flag as deleted
+// folder_name - move the message to the specified folder
+$config['calendar_itip_after_action'] = 0;
+
+// enable asynchronous free-busy triggering after data changed
+$config['calendar_freebusy_trigger'] = false;
+
+// free-busy information will be displayed for user calendars if available
+// 0 - no free-busy information
+// 1 - enabled in all views
+// 2 - only in quickview
+$config['calendar_include_freebusy_data'] = 1;
+
+// SMTP server host used to send (anonymous) itip messages.
+// Set to '' in order to use PHP's mail() function for email delivery.
+// To override the SMTP port or connection method, provide a full URL like 'tls://somehost:587'
+$config['calendar_itip_smtp_server'] = null;
+
+// SMTP username used to send (anonymous) itip messages
+$config['calendar_itip_smtp_user'] = 'smtpauth';
+
+// SMTP password used to send (anonymous) itip messages
+$config['calendar_itip_smtp_pass'] = '123456';
+
+// Base URL to build fully qualified URIs to access calendars via CALDAV
+// The following replacement variables are supported:
+// %h - Current HTTP host
+// %u - Current webmail user name
+// %n - Calendar name
+// %i - Calendar UUID
+// $config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i';
+
+// Crypt key to encrypt passwords for added iCAL/CalDAV calendars
+$config['calendar_crypt_key'] = "put some random string here";
+
+// Set to false to allow CURL to connect with SSL hosts that it can't verify the certificates from
+// e.g. for self-signed certificates.
+// technical note: This sets CURLOPT_SSL_VERIFYPEER _and_ CURLOPT_SSL_VERIFYHOST.
+$config['calendar_curl_secure_ssl'] = true;
+
+// Driver to provide a resource directory ('ldap' is the only implementation yet).
+// Leave empty or commented to disable resources support.
+// $config['calendar_resources_driver'] = 'ldap';
+
+// LDAP directory configuration to find avilable resources for events
+// $config['calendar_resources_directory'] = array(/* ldap_public-like address book configuration */);
+
+// Enable debugging output for iCAL/CalDAV drivers
+$config['calendar_caldav_debug'] = false;
+$config['calendar_ical_debug'] = false;
+
+// Pre-installed calendars, added at first access to calendar section
+// Caldav driver is supported only
+// $config['calendar_preinstalled_calendars'] = array(
+// 'Caldav' => array(
+// 'driver' => 'caldav',
+// 'caldav_user' => '%u',
+// 'caldav_pass' => '%p',
+// 'caldav_url' => 'http://example.caldav.org/%u/calendar/',
+// 'color' => 'cccc00',
+// 'showAlarms' => 1),
+// 'Other' => array(
+// 'driver' => 'other',
+// 'other_user' => 'user@example.other.org',
+// 'other_pass' => 'password',
+// 'other_url' => 'http://example.other.org/user@example.other.org/other',
+// 'color' => 'cc0000',
+// 'other_property1' => 'value1',
+// 'other_property2' => 'value2',
+// 'showAlarms' => 1));
+?>
diff --git a/calendar/drivers/caldav/SQL/mysql.initial.sql b/calendar/drivers/caldav/SQL/mysql.initial.sql
new file mode 100644
index 0000000..d60d482
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql.initial.sql
@@ -0,0 +1,92 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+CREATE TABLE IF NOT EXISTS `caldav_calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_user` varchar(255) DEFAULT NULL,
+ `caldav_pass` varchar(1024) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`calendar_id`),
+ INDEX `caldav_user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_caldav_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `instance` varchar(16) NOT NULL DEFAULT '',
+ `isexception` tinyint(1) NOT NULL DEFAULT '0',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` text NULL DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`event_id`),
+ INDEX `caldav_uid_idx` (`uid`),
+ INDEX `caldav_recurrence_idx` (`recurrence_id`),
+ INDEX `caldav_calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_caldav_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `caldav_calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_caldav_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `caldav_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2015022700'); \ No newline at end of file
diff --git a/calendar/drivers/caldav/SQL/mysql/.keep_dir b/calendar/drivers/caldav/SQL/mysql/.keep_dir
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/.keep_dir
diff --git a/calendar/drivers/caldav/SQL/mysql/2014081300.sql b/calendar/drivers/caldav/SQL/mysql/2014081300.sql
new file mode 100644
index 0000000..f1a3c98
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/2014081300.sql
@@ -0,0 +1,24 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+ALTER TABLE `caldav_props` change `user` `username` varchar(255);
+ALTER TABLE `events` ADD `status` VARCHAR(32) NOT NULL DEFAULT '' AFTER `sensitivity`; \ No newline at end of file
diff --git a/calendar/drivers/caldav/SQL/mysql/2015022500.sql b/calendar/drivers/caldav/SQL/mysql/2015022500.sql
new file mode 100644
index 0000000..df0f613
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/2015022500.sql
@@ -0,0 +1,125 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Create new tables */
+CREATE TABLE IF NOT EXISTS `caldav_calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_user` varchar(255) DEFAULT NULL,
+ `caldav_pass` varchar(1024) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`calendar_id`),
+ INDEX `caldav_user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_caldav_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` varchar(255) DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+
+ `caldav_url` varchar(255) NOT NULL,
+ `caldav_tag` varchar(255) DEFAULT NULL,
+ `caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`event_id`),
+ INDEX `caldav_uid_idx` (`uid`),
+ INDEX `caldav_recurrence_idx` (`recurrence_id`),
+ INDEX `caldav_calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_caldav_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `caldav_attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_caldav_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+/* Migrate Data */
+INSERT INTO caldav_calendars SELECT calendar_id, user_id, `name`, color, showalarms, url as caldav_url,
+ tag as caldav_tag, username as caldav_user, pass as caldav_pass,
+ last_change as caldav_last_change
+FROM calendars cal, caldav_props dav
+WHERE dav.obj_id = cal.calendar_id
+AND dav.obj_type = 'vcal';
+
+INSERT INTO caldav_events SELECT e.*, dav.url as caldav_url, dav.tag as caldav_tag, dav.last_change as caldav_last_change
+FROM `events` e, caldav_props dav
+WHERE dav.obj_id = e.event_id
+AND dav.obj_type = 'vevent';
+
+INSERT INTO caldav_attachments SELECT * FROM attachments a
+WHERE a.event_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+
+/* Drop deprecated data */
+DELETE FROM `events` WHERE event_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+DELETE FROM calendars WHERE calendar_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vcal'
+);
+DELETE FROM attachments WHERE event_id IN (
+ SELECT obj_id FROM caldav_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+DROP TABLE caldav_props;
+
diff --git a/calendar/drivers/caldav/SQL/mysql/2015022700.sql b/calendar/drivers/caldav/SQL/mysql/2015022700.sql
new file mode 100644
index 0000000..f44b49e
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/mysql/2015022700.sql
@@ -0,0 +1,14 @@
+-- add identifier for recurring instances and exceptions
+
+ALTER TABLE `caldav_events` ADD `instance` varchar(16) NOT NULL DEFAULT '' AFTER `uid`;
+ALTER TABLE `caldav_events` ADD `isexception` tinyint(1) NOT NULL DEFAULT '0' AFTER `instance`;
+
+UPDATE `caldav_events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%d')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 1;
+
+UPDATE `caldav_events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%dT%k%i%s')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 0;
+
+-- extend alarms columns for multiple values
+
+ALTER TABLE `caldav_events` CHANGE `alarms` `alarms` TEXT NULL DEFAULT NULL; \ No newline at end of file
diff --git a/calendar/drivers/caldav/SQL/postgres.initial.sql b/calendar/drivers/caldav/SQL/postgres.initial.sql
new file mode 100644
index 0000000..f49da4e
--- /dev/null
+++ b/calendar/drivers/caldav/SQL/postgres.initial.sql
@@ -0,0 +1,51 @@
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Hugo Slabbert <hugo@slabnet.com>
+ *
+ * Copyright (C) 2014, Hugo Slabbert <hugo@slabnet.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+CREATE TYPE caldav_type AS ENUM ('vcal','vevent','vtodo','');
+
+CREATE TABLE IF NOT EXISTS caldav_props (
+ obj_id int NOT NULL,
+ obj_type caldav_type NOT NULL,
+ url varchar(255) NOT NULL,
+ tag varchar(255) DEFAULT NULL,
+ username varchar(255) DEFAULT NULL,
+ pass varchar(1024) DEFAULT NULL,
+ last_change timestamp without time zone DEFAULT now() NOT NULL,
+ PRIMARY KEY (obj_id, obj_type)
+);
+
+CREATE OR REPLACE FUNCTION upd_timestamp() RETURNS TRIGGER
+LANGUAGE plpgsql
+AS
+$$
+BEGIN
+ NEW.last_change = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$;
+
+CREATE TRIGGER update_timestamp
+ BEFORE INSERT OR UPDATE
+ ON caldav_props
+ FOR EACH ROW
+ EXECUTE PROCEDURE upd_timestamp();
+
diff --git a/calendar/drivers/caldav/caldav_driver.php b/calendar/drivers/caldav/caldav_driver.php
new file mode 100644
index 0000000..b39aeff
--- /dev/null
+++ b/calendar/drivers/caldav/caldav_driver.php
@@ -0,0 +1,2036 @@
+<?php
+
+/**
+ * CalDAV driver for the Calendar plugin
+ *
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once (dirname(__FILE__).'/caldav_sync.php');
+require_once (dirname(__FILE__).'/../../lib/encryption.php');
+
+
+class caldav_driver extends calendar_driver
+{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
+ public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled');
+
+ // features this backend supports
+ public $alarms = true;
+ public $attendees = true;
+ public $freebusy = false;
+ public $attachments = true;
+ public $alarm_types = array('DISPLAY');
+
+ private $rc;
+ private $cal;
+ private $cache = array();
+ private $calendars = array();
+ private $calendar_ids = '';
+ private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
+ private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2);
+ private $server_timezone;
+
+ private $db_events = 'caldav_events';
+ private $db_calendars = 'caldav_calendars';
+ private $db_attachments = 'caldav_attachments';
+
+ // Crypt key for CalDAV auth
+ private $crypt_key;
+
+ // Holds CalDAV sync clients
+ private $sync_clients = array();
+
+ // Min. time period to wait until CalDAV sync check.
+ private $sync_period = 10; // seconds
+
+ // Indicates debug mode for CalDAV
+ static private $debug = null;
+
+ /**
+ * Helper method to log debug msg if debug mode is enabled.
+ */
+ static public function debug_log($msg)
+ {
+ if(self::$debug === true)
+ rcmail::console(__CLASS__.': '.$msg);
+ }
+
+ /**
+ * Helper method to log (if debug mode is enabled) and raise an user error.
+ */
+ private function _raise_error($msg)
+ {
+ self::debug_log($msg);
+ $this->rc->output->show_message($msg, 'error');
+ }
+
+ /**
+ * Default constructor
+ */
+ public function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->server_timezone = new DateTimeZone(date_default_timezone_get());
+
+ // read database config
+ $db = $this->rc->get_dbh();
+ $this->db_events = $this->rc->config->get('db_table_caldav_events', $db->table_name($this->db_events));
+ $this->db_calendars = $this->rc->config->get('db_table_caldav_calendars', $db->table_name($this->db_calendars));
+ $this->db_attachments = $this->rc->config->get('db_table_caldav_attachments', $db->table_name($this->db_attachments));
+ $this->crypt_key = $this->rc->config->get("calendar_crypt_key", "%E`c{2;<J2F^4_&._BxfQ<5Pf3qv!m{e");
+
+ // Set debug state
+ if(self::$debug === null)
+ self::$debug = $this->rc->config->get('calendar_caldav_debug', False);
+
+ $this->_read_calendars();
+ }
+
+ /**
+ * Read available calendars for the current user and store them internally
+ */
+ protected function _read_calendars()
+ {
+ $hidden = array_filter(explode(',', $this->rc->config->get('hidden_caldav_calendars', '')));
+
+ if (!empty($this->rc->user->ID)) {
+ $calendar_ids = array();
+ $result = $this->rc->db->query("SELECT *, calendar_id AS id
+ FROM " . $this->db_calendars . "
+ WHERE user_id=?
+ ORDER BY name",
+ $this->rc->user->ID
+ );
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $arr['showalarms'] = intval($arr['showalarms']);
+ $arr['active'] = !in_array($arr['id'], $hidden);
+ $arr['name'] = html::quote($arr['name']);
+ $arr['listname'] = html::quote($arr['name']);
+ $arr['rights'] = 'lrswikxteav';
+ $arr['editable'] = true;
+ $arr['caldav_pass'] = $this->_decrypt_pass($arr['caldav_pass']);
+ $this->calendars[$arr['calendar_id']] = $arr;
+ $calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
+
+ // Init sync client
+ $cal_id = $arr['calendar_id'];
+ $this->sync_clients[$cal_id] = new caldav_sync($arr);
+ }
+ $this->calendar_ids = join(',', $calendar_ids);
+ }
+ }
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer Bitmask defining filter criterias
+ *
+ * @return array List of calendars
+ */
+ public function list_calendars($filter = 0)
+ {
+ $calendars = $this->calendars;
+
+ // filter active calendars
+ if ($filter & self::FILTER_ACTIVE) {
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ }
+
+ // 'personal' is unsupported in this driver
+
+ return $calendars;
+ }
+
+ /**
+ * Extracts CalDAV calendar.
+ *
+ * @see database_driver::create_calendar()
+ */
+ public function create_calendar($cal)
+ {
+ $result = false;
+ $cal['caldav_url'] = self::_encode_url($cal["caldav_url"]);
+ if(!isset($cal['color'])) $cal['color'] = 'cc0000';
+
+ $calendars = $this->_autodiscover_calendars($this->_expand_pass($cal));
+ $cal_ids = array();
+
+ if($calendars)
+ {
+ $result = true;
+ foreach ($calendars as $calendar)
+ {
+ // Skip already existent calendars
+ $result = $this->rc->db->query("SELECT * FROM ".$this->db_calendars." WHERE user_id=? and caldav_url LIKE ?", $this->rc->user->ID, $calendar['href']);
+ if($this->rc->db->affected_rows($result)) continue;
+
+ $cal['caldav_url'] = self::_encode_url($calendar['href']);
+
+ // Respect $props['name'] if only a single calendar was found e.g. no auto-discovery.
+ if(sizeof($calendars) > 1 || !isset($cal['name']) || $cal['name'] == "")
+ $cal['name'] = $calendar['name'];
+
+ if (($obj_id = $this->_db_create_calendar($cal)) !== false) {
+ array_push($cal_ids, $obj_id);
+ } else $result = false;
+ }
+ }
+
+ // Sync newly created calendars
+ if($cal_ids) {
+
+ // Re-read calendars to internal buffer.
+ $this->_read_calendars();
+
+ // Initial sync of newly created calendars.
+ foreach ($cal_ids as $cal_id) {
+ $this->_sync_calendar($cal_id);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * caldav_url: CalDAV calendar URL
+ * caldav_tag: CalDAV calendar ctag
+ * caldav_user: CalDAV authentication user
+ * caldav_pass: CalDAV authentication password
+ *
+ * @return mixed ID of the calendar on success, False on error
+ */
+ private function _db_create_calendar($prop)
+ {
+ $result = $this->rc->db->query(
+ "INSERT INTO " . $this->db_calendars . "
+ (user_id, name, color, showalarms, caldav_url, caldav_tag, caldav_user, caldav_pass)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->rc->user->ID,
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms']?1:0,
+ $prop['caldav_url'],
+ isset($prop["caldav_tag"]) ? $prop["caldav_tag"] : null,
+ isset($prop["caldav_user"]) ? $prop["caldav_user"] : null,
+ isset($prop["caldav_pass"]) ? $this->_encrypt_pass($prop["caldav_pass"]) : null
+ );
+
+ if ($result)
+ return $this->rc->db->insert_id($this->db_calendars);
+
+ return false;
+ }
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function edit_calendar($cal)
+ {
+ $query = $this->rc->db->query("UPDATE " . $this->db_calendars . "
+ SET name=?, color=?, showalarms=?, caldav_url=?, caldav_tag=?, caldav_user=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $cal['name'],
+ $cal['color'],
+ $cal['showalarms']?1:0,
+ $cal['caldav_url'],
+ isset($cal["caldav_tag"]) ? $cal["caldav_tag"] : null,
+ isset($cal["caldav_user"]) ? $cal["caldav_user"] : null,
+ $cal['id'],
+ $this->rc->user->ID
+ );
+
+ // Change password if specified
+ if (isset($cal["caldav_pass"])) {
+ $query = $this->rc->db->query("UPDATE " . $this->db_calendars . "
+ SET caldav_pass=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $this->_encrypt_pass($cal['caldav_pass']),
+ $cal['id'],
+ $this->rc->user->ID
+ );
+ }
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Set active/subscribed state of a calendar
+ * Save a list of hidden calendars in user prefs
+ *
+ * @see calendar_driver::subscribe_calendar()
+ */
+ public function subscribe_calendar($prop)
+ {
+ $hidden = array_flip(explode(',', $this->rc->config->get('hidden_caldav_calendars', '')));
+
+ if ($prop['active'])
+ unset($hidden[$prop['id']]);
+ else
+ $hidden[$prop['id']] = 1;
+
+ return $this->rc->user->save_prefs(array('hidden_caldav_calendars' => join(',', array_keys($hidden))));
+ }
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @see calendar_driver::delete_calendar()
+ */
+ public function delete_calendar($prop)
+ {
+ if (!$this->calendars[$prop['id']])
+ return false;
+
+ // events and attachments will be deleted by foreign key cascade
+
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_calendars . " WHERE calendar_id=?",
+ $prop['id']
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Add a single event to the database
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::new_event()
+ */
+ public function new_event($event)
+ {
+ if (!$this->validate($event))
+ return false;
+
+ if (!empty($this->calendars)) {
+ if ($event['calendar'] && !$this->calendars[$event['calendar']])
+ return false;
+ if (!$event['calendar'])
+ $event['calendar'] = reset(array_keys($this->calendars));
+
+ if($event = $this->_save_preprocess($event)) {
+
+ $sync_client = $this->sync_clients[$event["calendar"]];
+
+ // Only push event if caldav_tag is not set to avoid pushing it twice
+ if (isset($event["caldav_tag"]) || ($event = $sync_client->create_event($event)) !== false) {
+
+ if ($event_id = $this->_insert_event($event)) {
+ $this->_update_recurring($event);
+ }
+ }
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ *
+ */
+ private function _insert_event(&$event)
+ {
+ //$event = $this->_save_preprocess($event);
+
+ $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence,
+ title, description, location, categories, url, free_busy, priority, sensitivity, status, attendees, alarms, notifyat,
+ caldav_url, caldav_tag)
+ VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['calendar'],
+ strval($event['uid']),
+ intval($event['recurrence_id']),
+ strval($event['_instance']),
+ intval($event['isexception']),
+ $event['start']->format(self::DB_DATE_FORMAT),
+ $event['end']->format(self::DB_DATE_FORMAT),
+ intval($event['all_day']),
+ $event['_recurrence'],
+ strval($event['title']),
+ strval($event['description']),
+ strval($event['location']),
+ join(',', (array)$event['categories']),
+ strval($event['url']),
+ intval($event['free_busy']),
+ intval($event['priority']),
+ intval($event['sensitivity']),
+ strval($event['status']),
+ $event['attendees'],
+ $event['alarms'],
+ $event['notifyat'],
+ $event['caldav_url'],
+ $event['caldav_tag']
+ );
+
+ $event_id = $this->rc->db->insert_id($this->db_events);
+
+ if ($event_id) {
+ $event['id'] = $event_id;
+
+ // add attachments
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event_id);
+ unset($attachment);
+ }
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the event entry with the given data and sync with caldav server.
+ *
+ * @param array Hash array with event properties
+ * @param array Internal use only, filled with non-modified event if this is second try after a calendar sync was enforced first.
+ * @see caldav_driver::_db_edit_event()
+ * @return bool
+ */
+ public function edit_event($event, $old_event = null)
+ {
+ $sync_enforced = ($old_event != null);
+ $event_id = (int)$event["id"];
+ $cal_id = $event["calendar"];
+
+ if($old_event == null)
+ $old_event = $this->get_event($event);
+
+ if($this->_db_edit_event($event))
+ {
+ // Re-load updated event and push to caldav.
+ $event = $this->get_event(array("id" => $event_id));
+
+ $sync_client = $this->sync_clients[$cal_id];
+ $success = $sync_client->update_event($event);
+
+ if($success === true)
+ {
+ self::debug_log("Successfully updated event \"$event_id\".");
+
+ // Trigger calendar sync to update ctags and etags.
+ $this->_sync_calendar($cal_id);
+
+ return true;
+ }
+ else if($success < 0 && $sync_enforced == false)
+ {
+ self::debug_log("Event \"$event_id\", tag \"".$event["caldav_tag"]."\" not up to date, will update calendar first ...");
+ $this->_sync_calendar($cal_id);
+
+ return $this->edit_event($event, $old_event); // Re-try after re-sync
+ }
+ else
+ {
+ $this->_db_edit_event($old_event);
+ $this->_raise_error("Could not update event: Unexpected CalDAV error.");
+
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::edit_event()
+ * @return bool
+ */
+ private function _db_edit_event($event)
+ {
+ if (!empty($this->calendars)) {
+ $update_master = false;
+ $update_recurring = true;
+ $old = $this->get_event($event);
+ $ret = true;
+
+ // check if update affects scheduling and update attendee status accordingly
+ $reschedule = $this->_check_scheduling($event, $old, true);
+
+ // increment sequence number
+ if (empty($event['sequence']) && $reschedule)
+ $event['sequence'] = max($event['sequence'], $old['sequence']) + 1;
+
+ // modify a recurring event, check submitted savemode to do the right things
+ if ($old['recurrence'] || $old['recurrence_id']) {
+ $master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
+
+ // keep saved exceptions (not submitted by the client)
+ if ($old['recurrence']['EXDATE'])
+ $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
+
+ switch ($event['_savemode']) {
+ case 'new':
+ $event['uid'] = $this->cal->generate_uid();
+ return $this->new_event($event);
+
+ case 'current':
+ // save as exception
+ $event['isexception'] = 1;
+ $update_recurring = false;
+
+ // set exception to first instance (= master)
+ if ($event['id'] == $master['id']) {
+ $event += $old;
+ $event['recurrence_id'] = $master['id'];
+ $event['_instance'] = libcalendaring::recurrence_instance_identifier($old);
+ $event['isexception'] = 1;
+ $event_id = $this->_insert_event($event);
+ return $event_id;
+ }
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event, then save this instance as new recurring event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // if recurrence COUNT, update value to the correct number of future occurences
+ if ($event['recurrence']['COUNT']) {
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $sqlresult = $this->rc->db->query(sprintf(
+ "SELECT event_id FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND %s >= ?
+ AND recurrence_id=?",
+ $this->calendar_ids,
+ $this->rc->db->quote_identifier('start')
+ ),
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']);
+ if ($count = $this->rc->db->num_rows($sqlresult))
+ $event['recurrence']['COUNT'] = $count;
+ }
+
+ $update_recurring = true;
+ $event['recurrence_id'] = 0;
+ $event['isexception'] = 0;
+ $event['_instance'] = '';
+ break;
+ }
+ // else: 'future' == 'all' if modifying the master event
+
+ default: // 'all' is default
+ $event['id'] = $master['id'];
+ $event['recurrence_id'] = 0;
+
+ // use start date from master but try to be smart on time or duration changes
+ $old_start_date = $old['start']->format('Y-m-d');
+ $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
+ $old_duration = $old['end']->format('U') - $old['start']->format('U');
+
+ $new_start_date = $event['start']->format('Y-m-d');
+ $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
+ $new_duration = $event['end']->format('U') - $event['start']->format('U');
+
+ $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
+
+ // shifted or resized
+ if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
+ $event['start'] = $master['start']->add($old['start']->diff($event['start']));
+ $event['end'] = clone $event['start'];
+ $event['end']->add(new DateInterval('PT' . $new_duration . 'S'));
+ } // dates did not change, use the ones from master
+ else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
+ $event['start'] = $master['start'];
+ $event['end'] = $master['end'];
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if (is_array($event['recurrence']) && ($old_start_date != $new_start_date || $old_start_time != $new_start_time)
+ && ($exceptions = $this->_load_exceptions($old))
+ ) {
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ foreach ($exceptions as $exception) {
+ $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ if (is_a($recurrence_id, 'DateTime')) {
+ $recurrence_id->add($date_shift);
+ $exception['_instance'] = $recurrence_id->format($recurrence_id_format);
+ $this->_update_event($exception, false);
+ }
+ }
+ }
+
+ $ret = $event['id']; // return master ID
+ break;
+ }
+ }
+
+ $success = $this->_update_event($event, $update_recurring);
+
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
+ $update_event = $this->get_event(array('id' => $event['recurrence_id']));
+ $update_event['_savemode'] = $event['_savemode'];
+ calendar::merge_attendee_data($update_event, $attendees);
+ }
+
+ if ($ret = $this->update_attendees($update_event, $attendees)) {
+ // replace $event with effectively updated event (for iTip reply)
+ if ($ret !== true && $ret != $update_event['id'] && ($new_event = $this->get_event(array('id' => $ret)))) {
+ $event = $new_event;
+ } else {
+ $event = $update_event;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ $success = $this->edit_event($event, true);
+
+ // apply attendee updates to recurrence exceptions too
+ if ($success && $event['_savemode'] == 'all' && !empty($event['recurrence']) && empty($event['recurrence_id']) && ($exceptions = $this->_load_exceptions($event))) {
+ foreach ($exceptions as $exception) {
+ calendar::merge_attendee_data($exception, $attendees);
+ $this->_update_event($exception, false);
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Determine whether the current change affects scheduling and reset attendee status accordingly
+ */
+ private function _check_scheduling(&$event, $old, $update = true)
+ {
+ // skip this check when importing iCal/iTip events
+ if (isset($event['sequence']) || !empty($event['_method'])) {
+ return false;
+ }
+
+ $reschedule = false;
+
+ // iterate through the list of properties considered 'significant' for scheduling
+ foreach (self::$scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $event[$prop];
+ if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
+ unset($a['EXCEPTIONS'], $b['EXCEPTIONS']);
+ $a = array_filter($a);
+ $b = array_filter($b);
+
+ // advanced rrule comparison: no rescheduling if series was shortened
+ if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
+ unset($a['COUNT'], $b['COUNT']);
+ } else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
+ unset($a['UNTIL'], $b['UNTIL']);
+ }
+ }
+ if ($a != $b) {
+ $reschedule = true;
+ break;
+ }
+ }
+
+ // reset all attendee status to needs-action (#4360)
+ if ($update && $reschedule && is_array($event['attendees'])) {
+ $is_organizer = false;
+ $emails = $this->cal->get_user_emails();
+ $attendees = $event['attendees'];
+ foreach ($attendees as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $is_organizer = true;
+ } else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
+ $attendees[$i]['status'] = 'NEEDS-ACTION';
+ $attendees[$i]['rsvp'] = true;
+ }
+ }
+
+ // update attendees only if I'm the organizer
+ if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
+ $event['attendees'] = $attendees;
+ }
+ }
+
+ return $reschedule;
+ }
+
+ /**
+ * Convert save data to be used in SQL statements
+ */
+ private function _save_preprocess($event)
+ {
+ // shift dates to server's timezone (except for all-day events)
+ if (!$event['allday']) {
+ $event['start'] = clone $event['start'];
+ $event['start']->setTimezone($this->server_timezone);
+ $event['end'] = clone $event['end'];
+ $event['end']->setTimezone($this->server_timezone);
+ }
+
+ // compose vcalendar-style recurrencue rule from structured data
+ $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
+ $event['_recurrence'] = rtrim($rrule, ';');
+ $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
+ $event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]);
+
+ if ($event['free_busy'] == 'tentative') {
+ $event['status'] = 'TENTATIVE';
+ }
+
+ if (isset($event['allday'])) {
+ $event['all_day'] = $event['allday'] ? 1 : 0;
+ }
+
+ // compute absolute time to notify the user
+ $event['notifyat'] = $this->_get_notification($event);
+
+ if (is_array($event['valarms'])) {
+ $event['alarms'] = $this->serialize_alarms($event['valarms']);
+ }
+
+ // process event attendees
+ if (!empty($event['attendees']))
+ $event['attendees'] = json_encode((array)$event['attendees']);
+ else
+ $event['attendees'] = '';
+
+ return $event;
+ }
+
+ /**
+ * Compute absolute time to notify the user
+ */
+ private function _get_notification($event)
+ {
+ if ($event['valarms'] && $event['start'] > new DateTime()) {
+ $alarm = libcalendaring::get_next_alarm($event);
+
+ if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
+ return date('Y-m-d H:i:s', $alarm['time']);
+ }
+
+ return null;
+ }
+
+ /**
+ * Save the given event record to database
+ *
+ * @param array Event data
+ * @param boolean True if recurring events instances should be updated, too
+ */
+ private function _update_event($event, $update_recurring = true)
+ {
+ $event = $this->_save_preprocess($event);
+ $sql_set = array();
+ $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat', 'caldav_url', 'caldav_tag');
+ foreach ($set_cols as $col) {
+ if (is_object($event[$col]) && is_a($event[$col], 'DateTime'))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT));
+ else if (is_array($event[$col]))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event[$col]));
+ else if (array_key_exists($col, $event))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
+ }
+
+ if ($event['_recurrence'])
+ $sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']);
+
+ if ($event['_instance'])
+ $sql_set[] = $this->rc->db->quote_identifier('instance') . '=' . $this->rc->db->quote($event['_instance']);
+
+ if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar'])
+ $sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']);
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s %s
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now(),
+ ($sql_set ? ', ' . join(', ', $sql_set) : '')
+ ),
+ $event['id']
+ );
+
+ $success = $this->rc->db->affected_rows($query);
+
+ // add attachments
+ if ($success && !empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event['id']);
+ unset($attachment);
+ }
+ }
+
+ // remove attachments
+ if ($success && !empty($event['deleted_attachments'])) {
+ foreach ($event['deleted_attachments'] as $attachment) {
+ $this->remove_attachment($attachment, $event['id']);
+ }
+ }
+
+ if ($success) {
+ unset($this->cache[$event['id']]);
+ if ($update_recurring)
+ $this->_update_recurring($event);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Insert "fake" entries for recurring occurences of this event
+ */
+ private function _update_recurring($event)
+ {
+ if (empty($this->calendars))
+ return;
+
+ if (!empty($event['recurrence'])) {
+ $exdata = array();
+ $exceptions = $this->_load_exceptions($event);
+
+ foreach ($exceptions as $exception) {
+ $exdate = substr($exception['_instance'], 0, 8);
+ $exdata[$exdate] = $exception;
+ }
+ }
+
+ // clear existing recurrence copies
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE recurrence_id=?
+ AND isexception=0
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $event['id']
+ );
+
+ // create new fake entries
+ if (!empty($event['recurrence'])) {
+ // include library class
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+
+ $recurrence = new calendar_recurrence($this->cal, $event);
+
+ $count = 0;
+ $event['allday'] = $event['all_day'];
+ $duration = $event['start']->diff($event['end']);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ while ($next_start = $recurrence->next_start()) {
+ $instance = $next_start->format($recurrence_id_format);
+ $datestr = substr($instance, 0, 8);
+
+ // skip exceptions
+ // TODO: merge updated data from master event
+ if ($exdata[$datestr]) {
+ continue;
+ }
+
+ $next_start->setTimezone($this->server_timezone);
+ $next_end = clone $next_start;
+ $next_end->add($duration);
+
+ $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end, 'status' => $event['status']));
+ $query = $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat, caldav_url, caldav_tag)
+ SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ?, caldav_url, caldav_tag
+ FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['id'],
+ $instance,
+ $next_start->format(self::DB_DATE_FORMAT),
+ $next_end->format(self::DB_DATE_FORMAT),
+ $notify_at,
+ $event['id']
+ );
+
+ if (!$this->rc->db->affected_rows($query))
+ break;
+
+ // stop adding events for inifinite recurrence after 20 years
+ if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20))
+ break;
+ }
+
+ // remove all exceptions after recurrence end
+ if ($next_end && !empty($exceptions)) {
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `start` > ?
+ AND `calendar_id` IN (" . $this->calendar_ids . ")",
+ $event['id'],
+ $next_end->format(self::DB_DATE_FORMAT)
+ );
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ private function _load_exceptions($event, $instance_id = null)
+ {
+ $sql_add_where = '';
+ if (!empty($instance_id)) {
+ $sql_add_where = 'AND `instance`=?';
+ }
+
+ $result = $this->rc->db->query(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `calendar_id` IN (" . $this->calendar_ids . ")
+ $sql_add_where
+ ORDER BY `instance`, `start`",
+ $event['id'],
+ $instance_id
+ );
+
+ $exceptions = array();
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $exception = $this->_read_postprocess($sql_arr);
+ $instance = $exception['_instance'] ?: $exception['start']->format($exception['allday'] ? 'Ymd' : 'Ymd\THis');
+ $exceptions[$instance] = $exception;
+ }
+
+ return $exceptions;
+ }
+
+ /**
+ * Move a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::move_event()
+ * @return bool
+ */
+ public function move_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Resize a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::resize_event()
+ * @return bool
+ */
+ public function resize_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Remove a single event from the database and from the CalDAV server.
+ *
+ * @param array Hash array with event properties
+ * @param boolean Remove record irreversible
+ *
+ * @see calendar_driver::remove_event()
+ * @return bool
+ */
+ public function remove_event($event, $force = true)
+ {
+ $event_id = (int)$event["id"];
+ $cal_id = (int)$event["calendar"];
+ $event = $this->get_event($event);
+
+ $sync_client = $this->sync_clients[$cal_id];
+ $success = $sync_client->remove_event($event);
+
+ if($success === true)
+ {
+ $this->_db_remove_event($event, $force);
+ self::debug_log("Successfully removed event \"$event_id\".");
+
+ // Trigger calendar sync to update ctags and etags.
+ $this->_sync_calendar($cal_id);
+
+ return true;
+ }
+
+ $this->_raise_error("Could not remove event: Unexpected CalDAV error.");
+ return false;
+ }
+
+ /**
+ * Remove a single event from the database
+ *
+ * @param array Hash array with event properties
+ * @param boolean Remove record irreversible (@TODO)
+ *
+ * @see calendar_driver::remove_event()
+ * @return bool
+ */
+ private function _db_remove_event($event, $force = true)
+ {
+ if (!empty($this->calendars)) {
+ $event += (array)$this->get_event($event);
+ $master = $event;
+ $update_master = false;
+ $savemode = 'all';
+ $ret = true;
+
+ // read master if deleting a recurring event
+ if ($event['recurrence'] || $event['recurrence_id']) {
+ $master = $event['recurrence_id'] ? $this->get_event(array('id' => $event['recurrence_id'])) : $event;
+ $savemode = $event['_savemode'];
+ }
+
+ switch ($savemode) {
+ case 'current':
+ // add exception to master event
+ $master['recurrence']['EXDATE'][] = $event['start'];
+ $update_master = true;
+
+ // just delete this single occurence
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND event_id=?",
+ $event['id']
+ );
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // delete this and all future instances
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND " . $this->rc->db->quote_identifier('start') . " >= ?
+ AND recurrence_id=?",
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']
+ );
+ $ret = $master['id'];
+ break;
+ }
+ // else: future == all if modifying the master event
+
+ default: // 'all' is default
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE (event_id=? OR recurrence_id=?)
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $master['id'],
+ $master['id']
+ );
+ break;
+ }
+
+ $success = $this->rc->db->affected_rows($query);
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return data of a specific event
+ * @param mixed Hash array with event properties or event UID
+ * @param integer Bitmask defining the scope to search events in
+ * @param boolean If true, recurrence exceptions shall be added
+ * @return array Hash array with event properties
+ */
+ public function get_event($event, $scope = 0, $full = false)
+ {
+ $id = is_array($event) ? ($event['id'] ?: $event['uid']) : $event;
+ $cal = is_array($event) ? $event['calendar'] : null;
+ $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid';
+
+ $where_add = '';
+ if (is_array($event) && !$event['id'] && !empty($event['_instance'])) {
+ $where_add = 'AND instance=' . $this->rc->db->quote($event['_instance']);
+ }
+
+ if ($this->cache[$id])
+ return $this->cache[$id];
+
+ if ($scope & self::FILTER_ACTIVE) {
+ $calendars = $this->calendars;
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ $cals = join(',', $calendars);
+ } else {
+ $cals = $this->calendar_ids;
+ }
+
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " AS e
+ WHERE e.calendar_id IN (%s)
+ AND e.$col=?
+ %s",
+ $cals,
+ $where_add
+ ),
+ $id);
+
+ if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $event = $this->_read_postprocess($sql_arr);
+
+ // also load recurrence exceptions
+ if (!empty($event['recurrence']) && $full) {
+ $event['recurrence']['EXCEPTIONS'] = array_values($this->_load_exceptions($event));
+ }
+
+ $this->cache[$id] = $event;
+ return $this->cache[$id];
+ }
+
+ return false;
+ }
+
+ /**
+ * Sync and returns event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_intersect($calendars, array_keys($this->calendars));
+
+ // Make sure that the calendars are in sync.
+ foreach ($calendar_ids as $cal_id) {
+ if (!$this->_is_synced($cal_id))
+ $this->_sync_calendar($cal_id);
+ }
+
+ return $this->_db_load_events($start, $end, $query, $calendars, $virtual, $modifiedsince);
+ }
+
+ /**
+ * Get event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ private function _db_load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars)));
+
+ // compose (slow) SQL query for searching
+ // FIXME: improve searching using a dedicated col and normalized values
+ if ($query) {
+ foreach (array('title', 'location', 'description', 'categories', 'attendees') as $col)
+ $sql_query[] = $this->rc->db->ilike($col, '%' . $query . '%');
+ $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
+ }
+
+ if (!$virtual)
+ $sql_add .= ' AND e.recurrence_id = 0';
+
+ if ($modifiedsince)
+ $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince));
+
+ $events = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " e
+ WHERE e.calendar_id IN (%s)
+ AND e.start <= %s AND e.end >= %s
+ %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($end),
+ $this->rc->db->fromunixtime($start),
+ $sql_add
+ ));
+
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) {
+ $event = $this->_read_postprocess($sql_arr);
+ $add = true;
+
+ if (!empty($event['recurrence']) && !$event['recurrence_id']) {
+ // load recurrence exceptions (i.e. for export)
+ if (!$virtual) {
+ $event['recurrence']['EXCEPTIONS'] = $this->_load_exceptions($event);
+ } // check for exception on first instance
+ else {
+ $instance = libcalendaring::recurrence_instance_identifier($event);
+ $exceptions = $this->_load_exceptions($event, $instance);
+ if ($exceptions && is_array($exceptions[$instance])) {
+ $event = $exceptions[$instance];
+ $add = false;
+ }
+ }
+ }
+
+ if ($add)
+ $events[] = $event;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ public function count_events($calendars, $start, $end = null)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Convert sql record into a rcube style event object
+ */
+ private function _read_postprocess($event)
+ {
+ $free_busy_map = array_flip($this->free_busy_map);
+ $sensitivity_map = array_flip($this->sensitivity_map);
+
+ $event['id'] = $event['event_id'];
+ $event['start'] = new DateTime($event['start']);
+ $event['end'] = new DateTime($event['end']);
+ $event['allday'] = intval($event['all_day']);
+ $event['created'] = new DateTime($event['created']);
+ $event['changed'] = new DateTime($event['changed']);
+ $event['free_busy'] = $free_busy_map[$event['free_busy']];
+ $event['sensitivity'] = $sensitivity_map[$event['sensitivity']];
+ $event['calendar'] = $event['calendar_id'];
+ $event['recurrence_id'] = intval($event['recurrence_id']);
+ $event['isexception'] = intval($event['isexception']);
+
+ // parse recurrence rule
+ if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
+ $event['recurrence'] = array();
+ foreach ($m as $rr) {
+ if (is_numeric($rr[2]))
+ $rr[2] = intval($rr[2]);
+ else if ($rr[1] == 'UNTIL')
+ $rr[2] = date_create($rr[2]);
+ else if ($rr[1] == 'RDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ else if ($rr[1] == 'EXDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ $event['recurrence'][$rr[1]] = $rr[2];
+ }
+ }
+
+ if ($event['recurrence_id']) {
+ libcalendaring::identify_recurrence_instance($event);
+ }
+
+ if (strlen($event['instance'])) {
+ $event['_instance'] = $event['instance'];
+
+ if (empty($event['recurrence_id'])) {
+ $event['recurrence_date'] = rcube_utils::anytodatetime($event['_instance'], $event['start']->getTimezone());
+ }
+ }
+
+ if ($event['_attachments'] > 0) {
+ $event['attachments'] = (array)$this->list_attachments($event);
+ }
+
+ // decode serialized event attendees
+ if (strlen($event['attendees'])) {
+ $event['attendees'] = $this->unserialize_attendees($event['attendees']);
+ } else {
+ $event['attendees'] = array();
+ }
+
+ // decode serialized alarms
+ if ($event['alarms']) {
+ $event['valarms'] = $this->unserialize_alarms($event['alarms']);
+ }
+
+ unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['instance'], $event['_attachments']);
+ return $event;
+ }
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @see calendar_driver::pending_alarms()
+ */
+ public function pending_alarms($time, $calendars = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (is_string($calendars))
+ $calendars = explode(',', $calendars);
+
+ // only allow to select from calendars with activated alarms
+ $calendar_ids = array();
+ foreach ($calendars as $cid) {
+ if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms'])
+ $calendar_ids[] = $cid;
+ }
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids);
+
+ $alarms = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND notifyat <= %s AND %s > %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($time),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->fromunixtime($time)
+ ));
+
+ while ($result && ($event = $this->rc->db->fetch_assoc($result)))
+ $alarms[] = $this->_read_postprocess($event);
+ }
+
+ return $alarms;
+ }
+
+ /**
+ * Feedback after showing/sending an alarm notification
+ *
+ * @see calendar_driver::dismiss_alarm()
+ */
+ public function dismiss_alarm($event_id, $snooze = 0)
+ {
+ // set new notifyat time or unset if not snoozed
+ $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null;
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s, notifyat=?
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now()),
+ $notify_at,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Save an attachment related to the given event
+ */
+ private function add_attachment($attachment, $event_id)
+ {
+ $data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
+
+ $query = $this->rc->db->query(
+ "INSERT INTO " . $this->db_attachments .
+ " (event_id, filename, mimetype, size, data)" .
+ " VALUES (?, ?, ?, ?, ?)",
+ $event_id,
+ $attachment['name'],
+ $attachment['mimetype'],
+ strlen($data),
+ base64_encode($data)
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Remove a specific attachment from the given event
+ */
+ private function remove_attachment($attachment_id, $event_id)
+ {
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_attachments .
+ " WHERE attachment_id = ?" .
+ " AND event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id = ?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))",
+ $attachment_id,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * List attachments of specified event
+ */
+ public function list_attachments($event)
+ {
+ $attachments = array();
+
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id=?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))".
+ " ORDER BY filename",
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id']
+ );
+
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $attachments[] = $arr;
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?",
+ $id,
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return $arr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get attachment body
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT data " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?",
+ $id,
+ $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return base64_decode($arr['data']);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=''
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=?
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name,
+ $oldname
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Helper method to serialize the list of alarms into a string
+ */
+ private function serialize_alarms($valarms)
+ {
+ foreach ((array)$valarms as $i => $alarm) {
+ if ($alarm['trigger'] instanceof DateTime) {
+ $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c');
+ }
+ }
+
+ return $valarms ? json_encode($valarms) : null;
+ }
+
+ /**
+ * Helper method to decode a serialized list of alarms
+ */
+ private function unserialize_alarms($alarms)
+ {
+ // decode json serialized alarms
+ if ($alarms && $alarms[0] == '[') {
+ $valarms = json_decode($alarms, true);
+ foreach ($valarms as $i => $alarm) {
+ if ($alarm['trigger'][0] == '@') {
+ try {
+ $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1));
+ }
+ catch (Exception $e) {
+ unset($valarms[$i]);
+ }
+ }
+ }
+ }
+ // convert legacy alarms data
+ else if (strlen($alarms)) {
+ list($trigger, $action) = explode(':', $alarms, 2);
+ if ($trigger = libcalendaring::parse_alarm_value($trigger)) {
+ $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
+ }
+ }
+
+ return $valarms;
+ }
+
+ /**
+ * Helper method to decode the attendees list from string
+ */
+ private function unserialize_attendees($s_attendees)
+ {
+ $attendees = array();
+
+ // decode json serialized string
+ if ($s_attendees[0] == '[') {
+ $attendees = json_decode($s_attendees, true);
+ } // decode the old serialization format
+ else {
+ foreach (explode("\n", $event['attendees']) as $line) {
+ $att = array();
+ foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) {
+ list($key, $value) = explode("=", $prop);
+ $att[strtolower($key)] = stripslashes(trim($value, '""'));
+ }
+ $attendees[] = $att;
+ }
+ }
+
+ return $attendees;
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ $db = $this->rc->db;
+ $user = $args['user'];
+ $event_ids = array();
+
+ $events = $db->query(
+ "SELECT event_id FROM " . $this->db_events . " AS ev" .
+ " LEFT JOIN " . $this->db_calendars . " cal ON (ev.calendar_id = cal.calendar_id)".
+ " WHERE user_id=?",
+ $user->ID);
+
+ while ($row = $db->fetch_assoc($events)) {
+ $event_ids[] = $row['event_id'];
+ }
+
+ if (!empty($event_ids)) {
+ foreach (array($this->db_attachments, $this->db_events) as $table) {
+ $db->query(sprintf("DELETE FROM $table WHERE event_id IN (%s)", join(',', $event_ids)));
+ }
+ }
+
+ foreach (array($this->db_calendars, 'itipinvitations') as $table) {
+ $db->query("DELETE FROM $table WHERE user_id=?", $user->ID);
+ }
+ }
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string Request action 'form-edit|form-new'
+ * @param array Calendar properties (e.g. id, color)
+ * @param array Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ // Make sure we have current attributes
+ $calendar = $this->calendars[$calendar["id"]];
+
+ $input_caldav_url = new html_inputfield( array(
+ "name" => "caldav_url",
+ "id" => "caldav_url",
+ "size" => 20
+ ));
+
+ $formfields["caldav_url"] = array(
+ "label" => $this->cal->gettext("caldavurl"),
+ "value" => $input_caldav_url->show($calendar["caldav_url"]),
+ "id" => "caldav_url",
+ );
+
+ $input_caldav_user = new html_inputfield( array(
+ "name" => "caldav_user",
+ "id" => "caldav_user",
+ "size" => 20
+ ));
+
+ $formfields["caldav_user"] = array(
+ "label" => $this->cal->gettext("username"),
+ "value" => $input_caldav_user->show($calendar["caldav_user"]),
+ "id" => "caldav_user",
+ );
+
+ $input_caldav_pass = new html_passwordfield( array(
+ "name" => "caldav_pass",
+ "id" => "caldav_pass",
+ "size" => 20
+ ));
+
+ $formfields["caldav_pass"] = array(
+ "label" => $this->cal->gettext("password"),
+ "value" => $input_caldav_pass->show(null), // Don't send plain text password to GUI
+ "id" => "caldav_pass",
+ );
+
+ return parent::calendar_form($action, $calendar, $formfields);
+ }
+
+ /**
+ * Encodes directory- and filenames using rawurlencode().
+ *
+ * @see http://stackoverflow.com/questions/7973790/urlencode-only-the-directory-and-file-names-of-a-url
+ * @param string Unencoded URL to be encoded.
+ * @return Encoded URL.
+ */
+ private static function _encode_url($url)
+ {
+ // Don't encode if "%" is already used.
+ if(strstr($url, "%") === false)
+ {
+ return preg_replace_callback('#://([^/]+)/([^?]+)#', function ($matches) {
+ return '://' . $matches[1] . '/' . join('/', array_map('rawurlencode', explode('/', $matches[2])));
+ }, $url);
+ }
+ else return $url;
+ }
+
+ /**
+ * Expand all "%p" occurrences in 'caldav_pass' element of calendar object
+ * properties array with RC (imap) password.
+ * Other elements are left untouched.
+ *
+ * @param array List of properties
+ * @return array List of properties, with expanded 'caldav_pass' attribute
+ *
+ */
+ private function _expand_pass($props)
+ {
+ if (isset($props['caldav_pass']))
+ $props['caldav_pass'] = str_replace('%p', $this->rc->get_user_password(), $props['caldav_pass']);
+
+ return $props;
+ }
+
+ /**
+ * Auto discover calenders available to the user on the caldav server
+ * @param array $props
+ * caldav_url: Absolute URL to CalDAV server
+ * caldav_user: Username
+ * caldav_pass: Password
+ * @return False on error or an array with the following calendar props:
+ * name: Calendar display name
+ * href: Absolute calendar URL
+ */
+ private function _autodiscover_calendars($props)
+ {
+ $calendars = array();
+ $current_user_principal = array('{DAV:}current-user-principal');
+ $calendar_home_set = array('{urn:ietf:params:xml:ns:caldav}calendar-home-set');
+ $cal_attribs = array('{DAV:}resourcetype', '{DAV:}displayname');
+
+ require_once ($this->cal->home.'/lib/caldav-client.php');
+ $caldav = new caldav_client($props["caldav_url"], $props["caldav_user"], $props["caldav_pass"]);
+
+ $tokens = parse_url($props["caldav_url"]);
+ $base_uri = $tokens['scheme']."://".$tokens['host'].($tokens['port'] ? ":".$tokens['port'] : null);
+ $caldav_url = $props["caldav_url"];
+ $response = $caldav->prop_find($caldav_url, array_merge($current_user_principal,$cal_attribs), 0);
+ if (!$response) {
+ $this->_raise_error("Resource \"$caldav_url\" has no collections");
+ return false;
+ }
+ else if (array_key_exists ('{DAV:}resourcetype', $response) &&
+ $response['{DAV:}resourcetype'] instanceof OldSabre\DAV\Property\ResourceType &&
+ in_array('{urn:ietf:params:xml:ns:caldav}calendar',
+ $response['{DAV:}resourcetype']->getValue())) {
+
+ $name = '';
+ if (array_key_exists ('{DAV:}displayname', $response)) {
+ $name = $response['{DAV:}displayname'];
+ }
+
+ array_push($calendars, array(
+ 'name' => $name,
+ 'href' => $caldav_url,
+ ));
+ return $calendars;
+ // directly return given url as it is a calendar
+ }
+ // probe further for principal url and user home set
+ $caldav_url = $base_uri . $response[$current_user_principal[0]];
+ $response = $caldav->prop_find($caldav_url, $calendar_home_set, 0);
+ if (!$response) {
+ $this->_raise_error("Resource \"$caldav_url\" contains no calendars.");
+ return false;
+ }
+ $caldav_url = $base_uri . $response[$calendar_home_set[0]];
+ $response = $caldav->prop_find($caldav_url, $cal_attribs, 1);
+ foreach($response as $collection => $attribs)
+ {
+ $found = false;
+ $name = '';
+ foreach($attribs as $key => $value)
+ {
+ if ($key == '{DAV:}resourcetype' && is_object($value)) {
+ if ($value instanceof OldSabre\DAV\Property\ResourceType) {
+ $values = $value->getValue();
+ if (in_array('{urn:ietf:params:xml:ns:caldav}calendar', $values))
+ $found = true;
+ }
+ }
+ else if ($key == '{DAV:}displayname') {
+ $name = $value;
+ }
+ }
+ if ($found) {
+ array_push($calendars, array(
+ 'name' => $name,
+ 'href' => $base_uri.$collection,
+ ));
+ }
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Synchronizes events of given calendar.
+ *
+ * @param int Calendar ID to sync
+ */
+ private function _sync_calendar($cal_id)
+ {
+ self::debug_log("Syncing calendar id \"$cal_id\".");
+
+ $cal_sync = $this->sync_clients[$cal_id];
+ $events = array();
+
+ // Ignore recurrence events and read caldav props
+ foreach($this->_load_all_events($cal_id) as $event) {
+ if($event["recurrence_id"] == 0) {
+ array_push($events, $event);
+ }
+ }
+
+ $updates = $cal_sync->get_updates($events);
+ if($updates)
+ {
+ list($updates, $synced_event_ids) = $updates;
+ $updated_event_ids = $this->_perform_updates($updates);
+
+ // Delete events that are not in sync or updated.
+ foreach($events as $event)
+ {
+ if(array_search($event["id"], $updated_event_ids) === false && // No updated event
+ array_search($event["id"], $synced_event_ids) === false) // No in-sync event
+ {
+ // Assume: Event not in sync and not updated, so delete!
+ $this->_db_remove_event($event, true);
+ self::debug_log("Remove event \"".$event["id"]."\".");
+ }
+ }
+
+ // Update calendar ctag ...
+ $calendar = $this->calendars[$cal_id];
+ $calendar["caldav_tag"] = $cal_sync->get_ctag();
+ $this->edit_calendar($calendar);
+ }
+
+ self::debug_log("Successfully synced calendar id \"$cal_id\".");
+ }
+
+ /**
+ * Return all events from the given calendar.
+ *
+ * @param int Calendar id.
+ * @return array
+ */
+ private function _load_all_events($cal_id)
+ {
+ // FIXME: This is kind of ugly but a way to get _all_ events without touching the database driver.
+
+ // Get the event with the maximum end time.
+ $result = $this->rc->db->query(
+ "SELECT MAX(e.end) as end FROM ".$this->db_events." e ".
+ "WHERE e.calendar_id = ? ", $cal_id);
+
+ if($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $end = new DateTime($arr["end"]);
+
+ // Don't use load_events() which is doing another sync while this method might be already invoked in an sync.
+ return $this->_db_load_events(0, $end->getTimestamp(), null, array($cal_id));
+ }
+ else return array();
+ }
+
+ /**
+ * Performs caldav updates on given events.
+ *
+ * @param array Caldav and event properties to update. See caldav_sync::get_updates().
+ * @return array List of event ids.
+ */
+ private function _perform_updates($updates)
+ {
+ $event_ids = array();
+
+ $num_created = 0;
+ $num_updated = 0;
+
+ foreach($updates as $update)
+ {
+ // local event -> update event
+ if(isset($update["local_event"]))
+ {
+ // Overwrite local event attributes with new event, url + etag.
+ $event = array_merge((array)$update["local_event"], $update["remote_event"], array(
+ "caldav_url" => $update["url"],
+ "caldav_tag" => $update["etag"]));
+
+ // let edit_event() do all the magic
+ if($this->_db_edit_event($event))
+ {
+ $event_id = $event["id"];
+ array_push($event_ids, $event_id);
+ $num_updated ++;
+ }
+ else
+ {
+ self::debug_log("Could not perform event update: ".print_r($update, true));
+ }
+ }
+
+ // no local event -> create event
+ else
+ {
+ $event = array_merge($update["remote_event"], array(
+ "caldav_url" => $update["url"],
+ "caldav_tag" => $update["etag"]));
+
+ $event_id = $this->new_event($event);
+ if($event_id)
+ {
+ self::debug_log("Created event \"$event_id\".");
+ array_push($event_ids, $event_id);
+ $num_created ++;
+ }
+ else
+ {
+ self::debug_log("Could not perform event creation: ".print_r($update, true));
+ }
+ }
+ }
+
+ self::debug_log("Created $num_created new events, updated $num_updated event.");
+ return $event_ids;
+ }
+
+ /**
+ * Determines whether the given calendar is in sync regarding
+ * calendar's ctag and the configured sync period.
+ *
+ * @param int Calender id.
+ * @return boolean True if calendar is in sync, true otherwise.
+ */
+ private function _is_synced($cal_id)
+ {
+ // Atomic sql: Check for exceeded sync period and update last_change.
+ $query = $this->rc->db->query(
+ "UPDATE ".$this->db_calendars." ".
+ "SET caldav_last_change = NOW() WHERE calendar_id = ? AND ".
+ $this->_unix_timestamp('caldav_last_change') ." + ? <= ".$this->_unix_timestamp('NOW()'),
+ $cal_id, $this->sync_period);
+
+ if($query->rowCount() > 0)
+ {
+ $is_synced = $this->sync_clients[$cal_id]->is_synced();
+ self::debug_log("Calendar \"$cal_id\" ".($is_synced ? "is in sync" : "needs update").".");
+ return $is_synced;
+ }
+ else
+ {
+ self::debug_log("Sync period active: Assuming calendar \"$cal_id\" to be in sync.");
+ return true;
+ }
+ }
+
+ /**
+ * Returns db-specific timestamp queries for epoch format
+ *
+ * @param str column name or valid timestamp (e.g. NOW())
+ * @return str db-specific timestamp query for epoch format
+ */
+ private function _unix_timestamp($field)
+ {
+ switch ($this->rc->db->db_provider) {
+ case 'postgres':
+ return "EXTRACT (EPOCH FROM $field)";
+ default:
+ return "UNIX_TIMESTAMP($field)";
+ }
+ }
+
+ private function _decrypt_pass($pass) {
+ $p = base64_decode($pass);
+ $e = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);
+ return $e->decrypt($p, $this->crypt_key);
+ }
+
+ private function _encrypt_pass($pass) {
+ $e = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);
+ $p = $e->encrypt($pass, $this->crypt_key);
+ return base64_encode($p);
+ }
+}
diff --git a/calendar/drivers/caldav/caldav_sync.php b/calendar/drivers/caldav/caldav_sync.php
new file mode 100644
index 0000000..efe92c2
--- /dev/null
+++ b/calendar/drivers/caldav/caldav_sync.php
@@ -0,0 +1,253 @@
+<?php
+/**
+ * CalDAV sync for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once (dirname(__FILE__).'/../../lib/caldav-client.php');
+
+class caldav_sync
+{
+ const ACTION_NONE = 1;
+ const ACTION_UPDATE = 2;
+ const ACTION_CREATE = 4;
+
+ private $cal_id = null;
+ private $ctag = null;
+ private $username = null;
+ private $pass = null;
+ private $url = null;
+
+ /**
+ * Default constructor for calendar synchronization adapter.
+ *
+ * @param array Hash array with caldav properties at least the following:
+ * id: Calendar ID
+ * caldav_url: Caldav calendar URL.
+ * caldav_user: Caldav http basic auth user.
+ * caldav_pass: Password für caldav user.
+ * caldav_tag: Caldav ctag for calendar.
+ */
+ public function __construct($cal)
+ {
+ $this->cal_id = $cal["id"];
+ $this->url = $cal["caldav_url"];
+ $this->ctag = isset($cal["caldav_tag"]) ? $cal["caldav_tag"] : null;
+ $this->username = isset($cal["caldav_user"]) ? $cal["caldav_user"] : null;
+ $this->pass = isset($cal["caldav_pass"]) ? $cal["caldav_pass"] : null;
+
+ $this->caldav = new caldav_client($this->url, $this->username, $this->pass);
+ }
+
+ /**
+ * Getter for current calendar ctag.
+ * @return string
+ */
+ public function get_ctag()
+ {
+ return $this->ctag;
+ }
+
+ /**
+ * Determines whether current calendar needs to be synced
+ * regarding the CalDAV ctag.
+ *
+ * @return True if the current calendar ctag differs from the CalDAV tag which
+ * indicates that there are changes that must be synched. Returns false
+ * if the calendar is up to date, no sync necesarry.
+ */
+ public function is_synced()
+ {
+ $is_synced = $this->ctag == $this->caldav->get_ctag() && $this->ctag;
+ caldav_driver::debug_log("Ctag indicates that calendar \"$this->cal_id\" ".($is_synced ? "is synced." : "needs update!"));
+
+ return $is_synced;
+ }
+
+ /**
+ * Synchronizes given events with caldav server and returns updates.
+ *
+ * @param array List of hash arrays with event properties, must include "caldav_url" and "tag".
+ * @return array Tuple containing the following lists:
+ *
+ * Caldav properties for events to be created or to be updated with the keys:
+ * url: Event ical URL relative to calendar URL
+ * etag: Remote etag of the event
+ * local_event: The local event in case of an update.
+ * remote_event: The current event retrieved from caldav server.
+ *
+ * A list of event ids that are in sync.
+ */
+ public function get_updates($events)
+ {
+ $ctag = $this->caldav->get_ctag();
+
+ if($ctag)
+ {
+ $this->ctag = $ctag;
+ $etags = $this->caldav->get_etags();
+
+ list($updates, $synced_event_ids) = $this->_get_event_updates($events, $etags);
+ return array($this->_get_event_data($updates), $synced_event_ids);
+ }
+ else
+ {
+ caldav_driver::debug_log("Unkown error while fetching calendar ctag for calendar \"$this->cal_id\"!");
+ }
+
+ return null;
+ }
+
+ /**
+ * Determines sync status and requried updates for the given events using given list of etags.
+ *
+ * @param array List of hash arrays with event properties, must include "caldav_url" and "caldav_tag".
+ * @param array List of current remote etags.
+ * @return array Tuple containing the following lists:
+ *
+ * Caldav properties for events to be created or to be updated with the keys:
+ * url: Event ical URL relative to calendar URL
+ * etag: Remote etag of the event
+ * local_event: The local event in case of an update.
+ *
+ * A list of event ids that are in sync.
+ */
+ private function _get_event_updates($events, $etags)
+ {
+ $updates = array();
+ $in_sync = array();
+
+ foreach ($etags as $etag)
+ {
+ $url = $etag["url"];
+ $etag = $etag["etag"];
+ $event_found = false;
+ foreach($events as $event)
+ {
+ if ($event["caldav_url"] == $url)
+ {
+ $event_found = true;
+
+ if ($event["caldav_tag"] != $etag)
+ {
+ caldav_driver::debug_log("Event ".$event["uid"]." needs update.");
+
+ array_push($updates, array(
+ "local_event" => $event,
+ "etag" => $etag,
+ "url" => $url
+ ));
+ }
+ else
+ {
+ array_push($in_sync, $event["id"]);
+ }
+ }
+ }
+
+ if (!$event_found)
+ {
+ caldav_driver::debug_log("Found new event ".$url);
+
+ array_push($updates, array(
+ "url" => $url,
+ "etag" => $etag
+ ));
+ }
+ }
+
+ return array($updates, $in_sync);
+ }
+
+ /**
+ * Fetches event data and attaches it to the given update properties.
+ *
+ * @param $updates List of update properties.
+ * @return array List of update properties with additional key "remote_event" containing the current caldav event.
+ */
+ private function _get_event_data($updates)
+ {
+ $urls = array();
+
+ foreach ($updates as $update)
+ {
+ array_push($urls, $update["url"]);
+ }
+
+ $events = $this->caldav->get_events($urls);
+ foreach($updates as &$update)
+ {
+ // Attach remote events to the appropriate updates.
+ // Note that this assumes unique event URL's!
+ $url = $update["url"];
+ if($events[$url]) {
+ $update["remote_event"] = $events[$url];
+ $update["remote_event"]["calendar"] = $this->cal_id;
+ }
+ }
+
+ return $updates;
+ }
+
+ /**
+ * Creates the given event on the CalDAV server.
+ *
+ * @param array Hash array with event properties.
+ * @return Event with updated "caldav_url" and "caldav_tag" attributes, false on error.
+ */
+ public function create_event($event)
+ {
+ $props = array(
+ "caldav_url" => parse_url($this->url, PHP_URL_PATH)."/".$event["uid"].".ics",
+ "caldav_tag" => null
+ );
+
+ caldav_driver::debug_log("Push new event to url ".$props["caldav_url"]);
+ $result = $this->caldav->put_event($props["caldav_url"], $event);
+
+ if($result == false || $result < 0) return false;
+ return array_merge($event, $props);
+ }
+
+ /**
+ * Updates the given event on the CalDAV server.
+ *
+ * @param array Hash array with event properties to update, must include "uid", "caldav_url" and "caldav_tag".
+ * @return True on success, false on error, -1 if the given event/etag is not up to date.
+ */
+ public function update_event($event)
+ {
+ caldav_driver::debug_log("Updating event uid \"".$event["uid"]."\".");
+ return $this->caldav->put_event($event["caldav_url"], $event, $event["caldav_tag"]);
+ }
+
+ /**
+ * Removes the given event from the caldav server.
+ *
+ * @param array Hash array with events properties, must include "caldav_url".
+ * @return True on success, false on error.
+ */
+ public function remove_event($event)
+ {
+ caldav_driver::debug_log("Removing event uid \"".$event["uid"]."\".");
+ return $this->caldav->remove_event($event["caldav_url"]);
+ }
+};
+?>
diff --git a/calendar/drivers/calendar_driver.php b/calendar/drivers/calendar_driver.php
new file mode 100644
index 0000000..3202003
--- /dev/null
+++ b/calendar/drivers/calendar_driver.php
@@ -0,0 +1,819 @@
+<?php
+
+/**
+ * Driver interface for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * Struct of an internal event object how it is passed from/to the driver classes:
+ *
+ * $event = array(
+ * 'id' => 'Event ID used for editing',
+ * 'uid' => 'Unique identifier of this event',
+ * 'calendar' => 'Calendar identifier to add event to or where the event is stored',
+ * 'start' => DateTime, // Event start date/time as DateTime object
+ * 'end' => DateTime, // Event end date/time as DateTime object
+ * 'allday' => true|false, // Boolean flag if this is an all-day event
+ * 'changed' => DateTime, // Last modification date of event
+ * 'title' => 'Event title/summary',
+ * 'location' => 'Location string',
+ * 'description' => 'Event description',
+ * 'url' => 'URL to more information',
+ * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs
+ * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY',
+ * 'INTERVAL' => 1...n,
+ * 'UNTIL' => DateTime,
+ * 'COUNT' => 1..n, // number of times
+ * // + more properties (see http://www.kanzaki.com/docs/ical/recur.html)
+ * 'EXDATE' => array(), // list of DateTime objects of exception Dates/Times
+ * 'EXCEPTIONS' => array(<event>), list of event objects which denote exceptions in the recurrence chain
+ * ),
+ * 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event
+ * '_instance' => 'ID of the recurring instance', // identifies an instance within a recurrence chain
+ * 'categories' => 'Event category',
+ * 'free_busy' => 'free|busy|outofoffice|tentative', // Show time as
+ * 'status' => 'TENTATIVE|CONFIRMED|CANCELLED', // event status according to RFC 2445
+ * 'priority' => 0-9, // Event priority (0=undefined, 1=highest, 9=lowest)
+ * 'sensitivity' => 'public|private|confidential', // Event sensitivity
+ * 'alarms' => '-15M:DISPLAY', // DEPRECATED Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before event)
+ * 'valarms' => array( // List of reminders (new format), each represented as a hash array:
+ * array(
+ * 'trigger' => '-PT90M', // ISO 8601 period string prefixed with '+' or '-', or DateTime object
+ * 'action' => 'DISPLAY|EMAIL|AUDIO',
+ * 'duration' => 'PT15M', // ISO 8601 period string
+ * 'repeat' => 0, // number of repetitions
+ * 'description' => '', // text to display for DISPLAY actions
+ * 'summary' => '', // message text for EMAIL actions
+ * 'attendees' => array(), // list of email addresses to receive alarm messages
+ * ),
+ * ),
+ * 'attachments' => array( // List of attachments
+ * 'name' => 'File name',
+ * 'mimetype' => 'Content type',
+ * 'size' => 1..n, // in bytes
+ * 'id' => 'Attachment identifier'
+ * ),
+ * 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated
+ * 'attendees' => array( // List of event participants
+ * 'name' => 'Participant name',
+ * 'email' => 'Participant e-mail address', // used as identifier
+ * 'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR',
+ * 'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED'
+ * 'rsvp' => true|false,
+ * ),
+ *
+ * '_savemode' => 'all|future|current|new', // How changes on recurring event should be handled
+ * '_notify' => true|false, // whether to notify event attendees about changes
+ * '_fromcalendar' => 'Calendar identifier where the event was stored before',
+ * );
+ */
+
+/**
+ * Interface definition for calendar driver classes
+ */
+abstract class calendar_driver
+{
+ const FILTER_ALL = 0;
+ const FILTER_WRITEABLE = 1;
+ const FILTER_INSERTABLE = 2;
+ const FILTER_ACTIVE = 4;
+ const FILTER_PERSONAL = 8;
+ const FILTER_PRIVATE = 16;
+ const FILTER_CONFIDENTIAL = 32;
+ const BIRTHDAY_CALENDAR_ID = '__bdays__';
+
+ // features supported by backend
+ public $alarms = false;
+ public $attendees = false;
+ public $freebusy = false;
+ public $attachments = false;
+ public $undelete = false;
+ public $history = false;
+ public $categoriesimmutable = false;
+ public $alarm_types = array('DISPLAY');
+ public $alarm_absolute = true;
+ public $last_error;
+
+ protected $default_categories = array(
+ 'Personal' => 'c0c0c0',
+ 'Work' => 'ff0000',
+ 'Family' => '00ff00',
+ 'Holiday' => 'ff6600',
+ );
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer Bitmask defining filter criterias.
+ * See FILTER_* constants for possible values.
+ * @return array List of calendars
+ */
+ abstract function list_calendars($filter = 0);
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * showalarms: True if alarms are enabled
+ * @return mixed ID of the calendar on success, False on error
+ */
+ abstract function create_calendar($prop);
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @param array Hash array with calendar properties
+ * id: Calendar Identifier
+ * name: Calendar name
+ * color: The color of the calendar
+ * showalarms: True if alarms are enabled (if supported)
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function edit_calendar($prop);
+
+ /**
+ * Set active/subscribed state of a calendar
+ *
+ * @param array Hash array with calendar properties
+ * id: Calendar Identifier
+ * active: True if calendar is active, false if not
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function subscribe_calendar($prop);
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @param array Hash array with calendar properties
+ * id: Calendar Identifier
+ * @return boolean True on success, Fales on failure
+ */
+ abstract function delete_calendar($prop);
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ abstract function search_calendars($query, $source);
+
+ /**
+ * Add a single event to the database
+ *
+ * @param array Hash array with event properties (see header of this file)
+ * @return mixed New event ID on success, False on error
+ */
+ abstract function new_event($event);
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array Hash array with event properties (see header of this file)
+ * @return boolean True on success, False on error
+ */
+ abstract function edit_event($event);
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ return $this->edit_event($event);
+ }
+
+ /**
+ * Update the participant status for the given attendee
+ *
+ * @param array Hash array with event properties
+ * @param array List of hash arrays each represeting an updated attendee
+ * @return boolean True on success, False on error
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ return $this->edit_event($event);
+ }
+
+ /**
+ * Move a single event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * start: Event start date/time as DateTime object
+ * end: Event end date/time as DateTime object
+ * allday: Boolean flag if this is an all-day event
+ * @return boolean True on success, False on error
+ */
+ abstract function move_event($event);
+
+ /**
+ * Resize a single event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * start: Event start date/time as DateTime object with timezone
+ * end: Event end date/time as DateTime object with timezone
+ * @return boolean True on success, False on error
+ */
+ abstract function resize_event($event);
+
+ /**
+ * Remove a single event from the database
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * @param boolean Remove event irreversible (mark as deleted otherwise,
+ * if supported by the backend)
+ *
+ * @return boolean True on success, False on error
+ */
+ abstract function remove_event($event, $force = true);
+
+ /**
+ * Restores a single deleted event (if supported)
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ *
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Return data of a single event
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * uid: Event UID
+ * _instance: Instance identifier in combination with uid (optional)
+ * calendar: Calendar identifier (optional)
+ * @param integer Bitmask defining the scope to search events in.
+ * See FILTER_* constants for possible values.
+ * @param boolean If true, recurrence exceptions shall be added
+ *
+ * @return array Event object as hash array
+ */
+ abstract function get_event($event, $scope = 0, $full = false);
+
+ /**
+ * Get events from source.
+ *
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @param string Search query (optional)
+ * @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
+ * @param boolean Include virtual/recurring events (optional)
+ * @param integer Only list events modified since this time (unix timestamp)
+ * @return array A list of event objects (see header of this file for struct of an event)
+ */
+ abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null);
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ abstract function count_events($calendars, $start, $end = null);
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @param integer Current time (unix timestamp)
+ * @param mixed List of calendar IDs to show alarms for (either as array or comma-separated string)
+ * @return array A list of alarms, each encoded as hash array:
+ * id: Event identifier
+ * uid: Unique identifier of this event
+ * start: Event start date/time as DateTime object
+ * end: Event end date/time as DateTime object
+ * allday: Boolean flag if this is an all-day event
+ * title: Event title/summary
+ * location: Location string
+ */
+ abstract function pending_alarms($time, $calendars = null);
+
+ /**
+ * (User) feedback after showing an alarm notification
+ * This should mark the alarm as 'shown' or snooze it for the given amount of time
+ *
+ * @param string Event identifier
+ * @param integer Suspend the alarm for this number of seconds
+ */
+ abstract function dismiss_alarm($event_id, $snooze = 0);
+
+ /**
+ * Check the given event object for validity
+ *
+ * @param array Event object as hash array
+ * @return boolean True if valid, false if not
+ */
+ public function validate($event)
+ {
+ $valid = true;
+
+ if (!is_object($event['start']) || !is_a($event['start'], 'DateTime'))
+ $valid = false;
+ if (!is_object($event['end']) || !is_a($event['end'], 'DateTime'))
+ $valid = false;
+
+ return $valid;
+ }
+
+
+ /**
+ * Get list of event's attachments.
+ * Drivers can return list of attachments as event property.
+ * If they will do not do this list_attachments() method will be used.
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of attachments, each as hash array:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
+ */
+ public function list_attachments($event) { }
+
+ /**
+ * Get attachment properties
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array Hash array with attachment properties:
+ * id: Attachment identifier
+ * name: Attachment name
+ * mimetype: MIME content type of the attachment
+ * size: Attachment size
+ */
+ public function get_attachment($id, $event) { }
+
+ /**
+ * Get attachment body
+ *
+ * @param string $id Attachment identifier
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return string Attachment body
+ */
+ public function get_attachment_body($id, $event) { }
+
+ /**
+ * Build a struct representing the given message reference
+ *
+ * @param object|string $uri_or_headers rcube_message_header instance holding the message headers
+ * or an URI from a stored link referencing a mail message.
+ * @param string $folder IMAP folder the message resides in
+ *
+ * @return array An struct referencing the given IMAP message
+ */
+ public function get_message_reference($uri_or_headers, $folder = null)
+ {
+ // to be implemented by the derived classes
+ return false;
+ }
+
+ /**
+ * List availabale categories
+ * The default implementation reads them from config/user prefs
+ */
+ public function list_categories()
+ {
+ $rcmail = rcube::get_instance();
+ return $rcmail->config->get('calendar_categories', $this->default_categories);
+ }
+
+ /**
+ * Create a new category
+ */
+ public function add_category($name, $color) { }
+
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name) { }
+
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color) { }
+
+ /**
+ * Fetch free/busy information from a person within the given range
+ *
+ * @param string E-mail address of attendee
+ * @param integer Requested period start date/time as unix timestamp
+ * @param integer Requested period end date/time as unix timestamp
+ *
+ * @return array List of busy timeslots within the requested range
+ */
+ public function get_freebusy_list($email, $start, $end)
+ {
+ return false;
+ }
+
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array Hash array with event properties
+ * @param object DateTime Start date of the recurrence window
+ * @param object DateTime End date of the recurrence window
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null)
+ {
+ $events = array();
+
+ if ($event['recurrence']) {
+ // include library class
+ require_once(dirname(__FILE__) . '/../lib/calendar_recurrence.php');
+
+ $rcmail = rcmail::get_instance();
+ $recurrence = new calendar_recurrence($rcmail->plugins->get_plugin('calendar'), $event);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+
+ // determine a reasonable end date if none given
+ if (!$end) {
+ switch ($event['recurrence']['FREQ']) {
+ case 'YEARLY': $intvl = 'P100Y'; break;
+ case 'MONTHLY': $intvl = 'P20Y'; break;
+ default: $intvl = 'P10Y'; break;
+ }
+
+ $end = clone $event['start'];
+ $end->add(new DateInterval($intvl));
+ }
+
+ $i = 0;
+ while ($next_event = $recurrence->next_instance()) {
+ // add to output if in range
+ if (($next_event['start'] <= $end && $next_event['end'] >= $start)) {
+ $next_event['_instance'] = $next_event['start']->format($recurrence_id_format);
+ $next_event['id'] = $next_event['uid'] . '-' . $next_event['_instance'];
+ $next_event['recurrence_id'] = $event['uid'];
+ $events[] = $next_event;
+ }
+ else if ($next_event['start'] > $end) { // stop loop if out of range
+ break;
+ }
+
+ // avoid endless recursion loops
+ if (++$i > 1000) {
+ break;
+ }
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ *
+ * @return array List of changes, each as a hash array:
+ * rev: Revision number
+ * type: Type of the change (create, update, move, delete)
+ * date: Change date
+ * user: The user who executed the change
+ * ip: Client IP
+ * destination: Destination calendar for 'move' type
+ */
+ public function get_event_changelog($event)
+ {
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revisions: "from:to"
+ *
+ * @return array List of property changes, each as a hash array:
+ * property: Revision number
+ * old: Old property value
+ * new: Updated property value
+ */
+ public function get_event_diff($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see self::get_event()
+ */
+ public function get_event_revison($event, $rev)
+ {
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ return false;
+ }
+
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string Request action 'form-edit|form-new'
+ * @param array Calendar properties (e.g. id, color)
+ * @param array Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ $html = '';
+ foreach ($formfields as $field) {
+ $html .= html::div('form-section',
+ html::label($field['id'], $field['label']) .
+ $field['value']);
+ }
+
+ return $html;
+ }
+
+ /**
+ * Compose a list of birthday events from the contact records in the user's address books.
+ *
+ * This is a default implementation using Roundcube's address book API.
+ * It can be overriden with a more optimized version by the individual drivers.
+ *
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param integer Only list events modified since this time (unix timestamp)
+ * @return array A list of event records
+ */
+ public function load_birthday_events($start, $end, $search = null, $modifiedsince = null)
+ {
+ // ignore update requests for simplicity reasons
+ if (!empty($modifiedsince)) {
+ return array();
+ }
+
+ // convert to DateTime for comparisons
+ $start = new DateTime('@'.$start);
+ $end = new DateTime('@'.$end);
+ // extract the current year
+ $year = $start->format('Y');
+ $year2 = $end->format('Y');
+
+ $events = array();
+ $search = mb_strtolower($search);
+ $rcmail = rcmail::get_instance();
+ $cache = $rcmail->get_cache('calendar.birthdays', 'db', 3600);
+ $cache->expunge();
+
+ $alarm_type = $rcmail->config->get('calendar_birthdays_alarm_type', '');
+ $alarm_offset = $rcmail->config->get('calendar_birthdays_alarm_offset', '-1D');
+ $alarms = $alarm_type ? $alarm_offset . ':' . $alarm_type : null;
+
+ // let the user select the address books to consider in prefs
+ $selected_sources = $rcmail->config->get('calendar_birthday_adressbooks');
+ $sources = $selected_sources ?: array_keys($rcmail->get_address_sources(false, true));
+ foreach ($sources as $source) {
+ $abook = $rcmail->get_address_book($source);
+
+ // skip LDAP address books unless selected by the user
+ if (!$abook || ($abook instanceof rcube_ldap && empty($selected_sources))) {
+ continue;
+ }
+
+ $abook->set_pagesize(10000);
+
+ // check for cached results
+ $cache_records = array();
+ $cached = $cache->get($source);
+
+ // iterate over (cached) contacts
+ foreach (($cached ?: $abook->search('*', '', 2, true, true, array('birthday'))) as $contact) {
+ if (is_array($contact) && !empty($contact['birthday'])) {
+ try {
+ if (is_array($contact['birthday']))
+ $contact['birthday'] = reset($contact['birthday']);
+
+ $bday = $contact['birthday'] instanceof DateTime ? $contact['birthday'] :
+ new DateTime($contact['birthday'], new DateTimezone('UTC'));
+ $birthyear = $bday->format('Y');
+ }
+ catch (Exception $e) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => 'BIRTHDAY PARSE ERROR: ' . $e),
+ true, false);
+ continue;
+ }
+
+ $display_name = rcube_addressbook::compose_display_name($contact);
+ $event_title = $rcmail->gettext(array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name)), 'calendar');
+
+ // add stripped record to cache
+ if (empty($cached)) {
+ $cache_records[] = array(
+ 'ID' => $contact['ID'],
+ 'name' => $display_name,
+ 'birthday' => $bday->format('Y-m-d'),
+ );
+ }
+
+ // filter by search term (only name is involved here)
+ if (!empty($search) && strpos(mb_strtolower($event_title), $search) === false) {
+ continue;
+ }
+
+ // quick-and-dirty recurrence computation: just replace the year
+ $bday->setDate($year, $bday->format('n'), $bday->format('j'));
+ $bday->setTime(12, 0, 0);
+
+ // date range reaches over multiple years: use end year if not in range
+ if (($bday > $end || $bday < $start) && $year2 != $year) {
+ $bday->setDate($year2, $bday->format('n'), $bday->format('j'));
+ $year = $year2;
+ }
+
+ // birthday is within requested range
+ if ($bday <= $end && $bday >= $start) {
+ $age = $year - $birthyear;
+ $event = array(
+ 'id' => rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $year),
+ 'calendar' => self::BIRTHDAY_CALENDAR_ID,
+ 'title' => $event_title,
+ 'description' => $rcmail->gettext(array('name' => 'birthdayage', 'vars' => array('age' => $age)), 'calendar'),
+ // Add more contact information to description block?
+ 'allday' => true,
+ 'start' => $bday,
+ 'alarms' => $alarms,
+ );
+ $event['end'] = clone $bday;
+ $event['end']->add(new DateInterval('PT1H'));
+
+ $events[] = $event;
+ }
+ }
+ }
+
+ // store collected contacts in cache
+ if (empty($cached)) {
+ $cache->write($source, $cache_records);
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get a single birthday calendar event
+ */
+ public function get_birthday_event($id)
+ {
+ // decode $id
+ list(,$source,$contact_id,$year) = explode(':', rcube_ldap::dn_decode($id));
+
+ $rcmail = rcmail::get_instance();
+
+ if ($source && $contact_id && ($abook = $rcmail->get_address_book($source))) {
+ $contact = $abook->get_record($contact_id, true);
+
+ if (is_array($contact) && !empty($contact['birthday'])) {
+ try {
+ if (is_array($contact['birthday']))
+ $contact['birthday'] = reset($contact['birthday']);
+
+ $bday = $contact['birthday'] instanceof DateTime ? $contact['birthday'] :
+ new DateTime($contact['birthday'], new DateTimezone('UTC'));
+ $birthyear = $bday->format('Y');
+ }
+ catch (Exception $e) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => 'BIRTHDAY PARSE ERROR: ' . $e),
+ true, false);
+
+ return null;
+ }
+
+ $display_name = rcube_addressbook::compose_display_name($contact);
+ $event_title = $rcmail->gettext(array('name' => 'birthdayeventtitle', 'vars' => array('name' => $display_name)), 'calendar');
+
+ $event = array(
+ 'id' => rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $year),
+ 'uid' => rcube_ldap::dn_encode('bday:' . $source . ':' . $contact['ID'] . ':' . $birthyear),
+ 'calendar' => self::BIRTHDAY_CALENDAR_ID,
+ 'title' => $event_title,
+ 'description' => '',
+ 'allday' => true,
+ 'start' => $bday,
+ 'recurrence' => array('FREQ' => 'YEARLY', 'INTERVAL' => 1),
+ 'free_busy' => 'free',
+ );
+ $event['end'] = clone $bday;
+ $event['end']->add(new DateInterval('PT1H'));
+
+ return $event;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Store alarm dismissal for birtual birthay events
+ *
+ * @param string Event identifier
+ * @param integer Suspend the alarm for this number of seconds
+ */
+ public function dismiss_birthday_alarm($event_id, $snooze = 0)
+ {
+ $rcmail = rcmail::get_instance();
+ $cache = $rcmail->get_cache('calendar.birthdayalarms', 'db', 86400 * 30);
+ $cache->remove($event_id);
+
+ // compute new notification time or disable if not snoozed
+ $notifyat = $snooze > 0 ? time() + $snooze : null;
+ $cache->set($event_id, array('snooze' => $snooze, 'notifyat' => $notifyat));
+
+ return true;
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ *
+ * @param array Hash array with hook arguments
+ * @return array Return arguments for plugin hooks
+ */
+ public function user_delete($args)
+ {
+ // TO BE OVERRIDDEN
+ return $args;
+ }
+}
diff --git a/calendar/drivers/database/SQL/mysql.initial.sql b/calendar/drivers/database/SQL/mysql.initial.sql
new file mode 100644
index 0000000..5f6dd60
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql.initial.sql
@@ -0,0 +1,85 @@
+/**
+ * Roundcube Calendar
+ *
+ * Plugin to add a calendar to Roundcube.
+ *
+ * @author Lazlo Westerhof
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ * @copyright (c) 2010 Lazlo Westerhof - Netherlands
+ * @copyright (c) 2014 Kolab Systems AG
+ *
+ **/
+
+CREATE TABLE IF NOT EXISTS `calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+ PRIMARY KEY(`calendar_id`),
+ INDEX `user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `instance` varchar(16) NOT NULL DEFAULT '',
+ `isexception` tinyint(1) NOT NULL DEFAULT '0',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` text DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+ PRIMARY KEY(`event_id`),
+ INDEX `uid_idx` (`uid`),
+ INDEX `recurrence_idx` (`recurrence_id`),
+ INDEX `calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `itipinvitations` (
+ `token` VARCHAR(64) NOT NULL,
+ `event_uid` VARCHAR(255) NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `event` TEXT NOT NULL,
+ `expires` DATETIME DEFAULT NULL,
+ `cancelled` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`token`),
+ INDEX `uid_idx` (`user_id`,`event_uid`),
+ CONSTRAINT `fk_itipinvitations_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+REPLACE INTO system (name, value) VALUES ('calendar-database-version', '2015022700');
diff --git a/calendar/drivers/database/SQL/mysql/2012080600.sql b/calendar/drivers/database/SQL/mysql/2012080600.sql
new file mode 100644
index 0000000..f38e1cf
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql/2012080600.sql
@@ -0,0 +1,3 @@
+-- MySQL database updates since version 0.7/0.8
+
+ALTER TABLE `events` ADD `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `changed`;
diff --git a/calendar/drivers/database/SQL/mysql/2013011000.sql b/calendar/drivers/database/SQL/mysql/2013011000.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql/2013011000.sql
@@ -0,0 +1 @@
+-- empty \ No newline at end of file
diff --git a/calendar/drivers/database/SQL/mysql/2013042700.sql b/calendar/drivers/database/SQL/mysql/2013042700.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql/2013042700.sql
@@ -0,0 +1 @@
+-- empty \ No newline at end of file
diff --git a/calendar/drivers/database/SQL/mysql/2013051600.sql b/calendar/drivers/database/SQL/mysql/2013051600.sql
new file mode 100644
index 0000000..4de44d6
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql/2013051600.sql
@@ -0,0 +1,3 @@
+-- MySQL database updates since version 0.9-beta
+
+ALTER TABLE `events` ADD `url` VARCHAR(255) NOT NULL AFTER `categories`; \ No newline at end of file
diff --git a/calendar/drivers/database/SQL/mysql/2014040900.sql b/calendar/drivers/database/SQL/mysql/2014040900.sql
new file mode 100644
index 0000000..814e10d
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql/2014040900.sql
@@ -0,0 +1,3 @@
+-- MySQL database updates since version 1.0
+
+ALTER TABLE `events` ADD `status` VARCHAR(32) NOT NULL AFTER `sensitivity`;
diff --git a/calendar/drivers/database/SQL/mysql/2015022700.sql b/calendar/drivers/database/SQL/mysql/2015022700.sql
new file mode 100644
index 0000000..06d30fe
--- /dev/null
+++ b/calendar/drivers/database/SQL/mysql/2015022700.sql
@@ -0,0 +1,15 @@
+-- add identifier for recurring instances and exceptions
+
+ALTER TABLE `events` ADD `instance` varchar(16) NOT NULL DEFAULT '' AFTER `uid`;
+ALTER TABLE `events` ADD `isexception` tinyint(1) NOT NULL DEFAULT '0' AFTER `instance`;
+
+UPDATE `events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%d')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 1;
+
+UPDATE `events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%dT%k%i%s')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 0;
+
+-- extend alarms columns for multiple values
+
+ALTER TABLE `events` CHANGE `alarms` `alarms` TEXT NULL DEFAULT NULL;
+
diff --git a/calendar/drivers/database/SQL/postgres.initial.sql b/calendar/drivers/database/SQL/postgres.initial.sql
new file mode 100644
index 0000000..b170086
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres.initial.sql
@@ -0,0 +1,109 @@
+/**
+ * RoundCube Calendar
+ *
+ * Plugin to add a calendar to RoundCube.
+ *
+ * @author Lazlo Westerhof
+ * @author Albert Lee
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ * @licence GNU AGPL
+ * @copyright (c) 2010 Lazlo Westerhof - Netherlands
+ * @copyright (c) 2014 Kolab Systems AG
+ *
+ **/
+
+
+CREATE SEQUENCE calendars_seq
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+
+CREATE TABLE calendars (
+ calendar_id integer DEFAULT nextval('calendars_seq'::regclass) NOT NULL,
+ user_id integer NOT NULL
+ REFERENCES users (user_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ name varchar(255) NOT NULL,
+ color varchar(8) NOT NULL,
+ showalarms smallint NOT NULL DEFAULT 1,
+ PRIMARY KEY (calendar_id)
+);
+
+CREATE INDEX calendars_user_id_idx ON calendars (user_id, name);
+
+
+CREATE SEQUENCE events_seq
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+
+CREATE TABLE events (
+ event_id integer DEFAULT nextval('events_seq'::regclass) NOT NULL,
+ calendar_id integer NOT NULL
+ REFERENCES calendars (calendar_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ recurrence_id integer NOT NULL DEFAULT 0,
+ uid varchar(255) NOT NULL DEFAULT '',
+ instance varchar(16) NOT NULL DEFAULT '',
+ isexception smallint NOT NULL DEFAULT '0',
+ created timestamp without time zone DEFAULT now() NOT NULL,
+ changed timestamp without time zone DEFAULT now(),
+ sequence integer NOT NULL DEFAULT 0,
+ "start" timestamp without time zone DEFAULT now() NOT NULL,
+ "end" timestamp without time zone DEFAULT now() NOT NULL,
+ recurrence varchar(255) DEFAULT NULL,
+ title character varying(255) NOT NULL DEFAULT '',
+ description text NOT NULL DEFAULT '',
+ location character varying(255) NOT NULL DEFAULT '',
+ categories character varying(255) NOT NULL DEFAULT '',
+ url character varying(255) NOT NULL DEFAULT '',
+ all_day smallint NOT NULL DEFAULT 0,
+ free_busy smallint NOT NULL DEFAULT 0,
+ priority smallint NOT NULL DEFAULT 0,
+ sensitivity smallint NOT NULL DEFAULT 0,
+ status character varying(32) NOT NULL DEFAULT '',
+ alarms text DEFAULT NULL,
+ attendees text DEFAULT NULL,
+ notifyat timestamp without time zone DEFAULT NULL,
+ PRIMARY KEY (event_id)
+);
+
+CREATE INDEX events_calendar_id_notifyat_idx ON events (calendar_id, notifyat);
+CREATE INDEX events_uid_idx ON events (uid);
+CREATE INDEX events_recurrence_id_idx ON events (recurrence_id);
+
+
+CREATE SEQUENCE attachments_seq
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;
+
+CREATE TABLE attachments (
+ attachment_id integer DEFAULT nextval('attachments_seq'::regclass) NOT NULL,
+ event_id integer NOT NULL
+ REFERENCES events (event_id) ON DELETE CASCADE ON UPDATE CASCADE,
+ filename varchar(255) NOT NULL DEFAULT '',
+ mimetype varchar(255) NOT NULL DEFAULT '',
+ size integer NOT NULL DEFAULT 0,
+ data text NOT NULL DEFAULT '',
+ PRIMARY KEY (attachment_id)
+);
+
+CREATE INDEX attachments_user_id_idx ON attachments (event_id);
+
+
+CREATE TABLE itipinvitations (
+ token varchar(64) NOT NULL,
+ event_uid varchar(255) NOT NULL,
+ user_id integer NOT NULL
+ REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+ event TEXT NOT NULL,
+ expires timestamp without time zone DEFAULT NULL,
+ cancelled smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY (token)
+);
+
+CREATE INDEX itipinvitations_user_id_event_uid_idx ON itipinvitations (user_id, event_uid);
+
+INSERT INTO system (name, value) VALUES ('calendar-database-version', '2015022700');
diff --git a/calendar/drivers/database/SQL/postgres/2012080600.sql b/calendar/drivers/database/SQL/postgres/2012080600.sql
new file mode 100644
index 0000000..9a273e6
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres/2012080600.sql
@@ -0,0 +1,3 @@
+-- Postgres database updates since version 0.7/0.8
+
+ALTER TABLE events ADD sequence integer NOT NULL DEFAULT 0;
diff --git a/calendar/drivers/database/SQL/postgres/2013011000.sql b/calendar/drivers/database/SQL/postgres/2013011000.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres/2013011000.sql
@@ -0,0 +1 @@
+-- empty \ No newline at end of file
diff --git a/calendar/drivers/database/SQL/postgres/2013042700.sql b/calendar/drivers/database/SQL/postgres/2013042700.sql
new file mode 100644
index 0000000..d644c39
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres/2013042700.sql
@@ -0,0 +1,8 @@
+ALTER SEQUENCE calendar_ids RENAME TO calendars_seq;
+ALTER TABLE calendars ALTER COLUMN calendar_id SET DEFAULT nextval('calendars_seq'::text);
+
+ALTER SEQUENCE event_ids RENAME TO events_seq;
+ALTER TABLE events ALTER COLUMN event_id SET DEFAULT nextval('events_seq'::text);
+
+ALTER SEQUENCE attachment_ids RENAME TO attachments_seq;
+ALTER TABLE attachments ALTER COLUMN attachment_id SET DEFAULT nextval('attachments_seq'::text);
diff --git a/calendar/drivers/database/SQL/postgres/2013051600.sql b/calendar/drivers/database/SQL/postgres/2013051600.sql
new file mode 100644
index 0000000..3c1da43
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres/2013051600.sql
@@ -0,0 +1,3 @@
+-- Postgres database updates since version 0.9-beta
+
+ALTER TABLE events ADD url character varying(255) NOT NULL;
diff --git a/calendar/drivers/database/SQL/postgres/2014040900.sql b/calendar/drivers/database/SQL/postgres/2014040900.sql
new file mode 100644
index 0000000..310744c
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres/2014040900.sql
@@ -0,0 +1,3 @@
+-- Postgres database updates since version 1.0
+
+ALTER TABLE events ADD status character varying(32) NOT NULL;
diff --git a/calendar/drivers/database/SQL/postgres/2015022700.sql b/calendar/drivers/database/SQL/postgres/2015022700.sql
new file mode 100644
index 0000000..0de989e
--- /dev/null
+++ b/calendar/drivers/database/SQL/postgres/2015022700.sql
@@ -0,0 +1,9 @@
+-- add identifier for recurring instances and exceptions
+
+ALTER TABLE events ADD instance character varying(16) NOT NULL;
+ALTER TABLE events ADD isexception smallint NOT NULL DEFAULT '0';
+
+-- extend alarms columns for multiple values
+
+ALTER TABLE events ALTER COLUMN alarms TYPE text;
+
diff --git a/calendar/drivers/database/SQL/sqlite.initial.sql b/calendar/drivers/database/SQL/sqlite.initial.sql
new file mode 100644
index 0000000..c8aa971
--- /dev/null
+++ b/calendar/drivers/database/SQL/sqlite.initial.sql
@@ -0,0 +1,79 @@
+/**
+ * Roundcube Calendar
+ *
+ * Plugin to add a calendar to Roundcube.
+ *
+ * @author Lazlo Westerhof
+ * @author Thomas Bruederli
+ * @author Albert Lee
+ * @licence GNU AGPL
+ * @copyright (c) 2010 Lazlo Westerhof - Netherlands
+ * @copyright (c) 2014 Kolab Systems AG
+ *
+ **/
+
+CREATE TABLE calendars (
+ calendar_id integer NOT NULL PRIMARY KEY,
+ user_id integer NOT NULL default '0',
+ name varchar(255) NOT NULL default '',
+ color varchar(255) NOT NULL default '',
+ showalarms tinyint(1) NOT NULL default '1',
+ CONSTRAINT fk_calendars_user_id FOREIGN KEY (user_id)
+ REFERENCES users(user_id)
+);
+
+CREATE TABLE events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ instance varchar(16) NOT NULL default '',
+ isexception tinyint(1) NOT NULL default '0',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ url varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ status varchar(32) NOT NULL default '',
+ alarms text default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL,
+ CONSTRAINT fk_events_calendar_id FOREIGN KEY (calendar_id)
+ REFERENCES calendars(calendar_id)
+);
+
+CREATE TABLE attachments (
+ attachment_id integer NOT NULL PRIMARY KEY,
+ event_id integer NOT NULL default '0',
+ filename varchar(255) NOT NULL default '',
+ mimetype varchar(255) NOT NULL default '',
+ size integer NOT NULL default '0',
+ data text NOT NULL default '',
+ CONSTRAINT fk_attachment_event_id FOREIGN KEY (event_id)
+ REFERENCES events(event_id)
+);
+
+CREATE TABLE itipinvitations (
+ token varchar(64) NOT NULL PRIMARY KEY,
+ event_uid varchar(255) NOT NULL,
+ user_id integer NOT NULL default '0',
+ event text NOT NULL,
+ expires datetime NOT NULL default '1000-01-01 00:00:00',
+ cancelled tinyint(1) NOT NULL default '0',
+ CONSTRAINT fk_itipinvitations_user_id FOREIGN KEY (user_id)
+ REFERENCES users(user_id)
+);
+
+CREATE INDEX ix_itipinvitations_uid ON itipinvitations(user_id, event_uid);
+
+INSERT INTO system (name, value) VALUES ('calendar-database-version', '2015022700');
diff --git a/calendar/drivers/database/SQL/sqlite/2013011000.sql b/calendar/drivers/database/SQL/sqlite/2013011000.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/calendar/drivers/database/SQL/sqlite/2013011000.sql
@@ -0,0 +1 @@
+-- empty \ No newline at end of file
diff --git a/calendar/drivers/database/SQL/sqlite/2013042700.sql b/calendar/drivers/database/SQL/sqlite/2013042700.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/calendar/drivers/database/SQL/sqlite/2013042700.sql
@@ -0,0 +1 @@
+-- empty \ No newline at end of file
diff --git a/calendar/drivers/database/SQL/sqlite/2013051600.sql b/calendar/drivers/database/SQL/sqlite/2013051600.sql
new file mode 100644
index 0000000..850fae3
--- /dev/null
+++ b/calendar/drivers/database/SQL/sqlite/2013051600.sql
@@ -0,0 +1,63 @@
+-- SQLite database updates since version 0.9-beta
+
+-- ALTER TABLE events ADD url varchar(255) NOT NULL AFTER categories;
+
+CREATE TABLE temp_events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ alarms varchar(255) default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL
+);
+
+INSERT INTO temp_events (event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat)
+ SELECT event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat FROM events;
+
+DROP TABLE events;
+
+CREATE TABLE events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ url varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ alarms varchar(255) default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL,
+ CONSTRAINT fk_events_calendar_id FOREIGN KEY (calendar_id)
+ REFERENCES calendars(calendar_id)
+);
+
+INSERT INTO events (event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat)
+ SELECT event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat FROM temp_events;
+
diff --git a/calendar/drivers/database/SQL/sqlite/2014040900.sql b/calendar/drivers/database/SQL/sqlite/2014040900.sql
new file mode 100644
index 0000000..ff8ed17
--- /dev/null
+++ b/calendar/drivers/database/SQL/sqlite/2014040900.sql
@@ -0,0 +1,67 @@
+-- SQLite database updates since version 0.9-beta
+
+-- ALTER TABLE events ADD url varchar(255) NOT NULL AFTER categories;
+
+CREATE TABLE temp_events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ url varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ alarms varchar(255) default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL
+);
+
+INSERT INTO temp_events (event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat)
+ SELECT event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat
+ FROM events;
+
+DROP TABLE events;
+
+CREATE TABLE events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ url varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ status varchar(32) NOT NULL default '',
+ alarms varchar(255) default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL,
+ CONSTRAINT fk_events_calendar_id FOREIGN KEY (calendar_id)
+ REFERENCES calendars(calendar_id)
+);
+
+INSERT INTO events (event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat)
+ SELECT event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat
+ FROM temp_events;
+
diff --git a/calendar/drivers/database/SQL/sqlite/2015022700.sql b/calendar/drivers/database/SQL/sqlite/2015022700.sql
new file mode 100644
index 0000000..9770701
--- /dev/null
+++ b/calendar/drivers/database/SQL/sqlite/2015022700.sql
@@ -0,0 +1,79 @@
+-- ALTER TABLE `events` ADD `instance` varchar(16) NOT NULL DEFAULT '' AFTER `uid`;
+-- ALTER TABLE `events` ADD `isexception` tinyint(3) NOT NULL DEFAULT '0' AFTER `instance`;
+-- ALTER TABLE `events` CHANGE `alarms` `alarms` TEXT NULL DEFAULT NULL;
+
+CREATE TABLE temp_events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ url varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ status varchar(32) NOT NULL default '',
+ alarms varchar(255) default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL
+);
+
+INSERT INTO temp_events (event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat)
+ SELECT event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat
+ FROM events;
+
+DROP TABLE events;
+
+CREATE TABLE events (
+ event_id integer NOT NULL PRIMARY KEY,
+ calendar_id integer NOT NULL default '0',
+ recurrence_id integer NOT NULL default '0',
+ uid varchar(255) NOT NULL default '',
+ instance varchar(16) NOT NULL default '',
+ isexception tinyint(1) NOT NULL default '0',
+ created datetime NOT NULL default '1000-01-01 00:00:00',
+ changed datetime NOT NULL default '1000-01-01 00:00:00',
+ sequence integer NOT NULL default '0',
+ start datetime NOT NULL default '1000-01-01 00:00:00',
+ end datetime NOT NULL default '1000-01-01 00:00:00',
+ recurrence varchar(255) default NULL,
+ title varchar(255) NOT NULL,
+ description text NOT NULL,
+ location varchar(255) NOT NULL default '',
+ categories varchar(255) NOT NULL default '',
+ url varchar(255) NOT NULL default '',
+ all_day tinyint(1) NOT NULL default '0',
+ free_busy tinyint(1) NOT NULL default '0',
+ priority tinyint(1) NOT NULL default '0',
+ sensitivity tinyint(1) NOT NULL default '0',
+ status varchar(32) NOT NULL default '',
+ alarms text default NULL,
+ attendees text default NULL,
+ notifyat datetime default NULL,
+ CONSTRAINT fk_events_calendar_id FOREIGN KEY (calendar_id)
+ REFERENCES calendars(calendar_id)
+);
+
+INSERT INTO events (event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat)
+ SELECT event_id, calendar_id, recurrence_id, uid, created, changed, sequence, start, end, recurrence, title, description, location, categories, url, all_day, free_busy, priority, sensitivity, alarms, attendees, notifyat
+ FROM temp_events;
+
+DROP TABLE temp_events;
+
+-- Derrive instance columns from start date/time
+
+UPDATE events SET instance = strftime('%Y%m%d', start)
+ WHERE recurrence_id != 0 AND instance = '' AND all_day = 1;
+
+UPDATE events SET instance = strftime('%Y%m%dT%H%M%S', start)
+ WHERE recurrence_id != 0 AND instance = '' AND all_day = 0;
diff --git a/calendar/drivers/database/database_driver.php b/calendar/drivers/database/database_driver.php
new file mode 100644
index 0000000..51c5afc
--- /dev/null
+++ b/calendar/drivers/database/database_driver.php
@@ -0,0 +1,1496 @@
+<?php
+
+/**
+ * Database driver for the Calendar plugin
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class database_driver extends calendar_driver
+{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
+ public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled');
+
+ // features this backend supports
+ public $alarms = true;
+ public $attendees = true;
+ public $freebusy = false;
+ public $attachments = true;
+ public $alarm_types = array('DISPLAY');
+
+ private $rc;
+ private $cal;
+ private $cache = array();
+ private $calendars = array();
+ private $calendar_ids = '';
+ private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
+ private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2);
+ private $server_timezone;
+
+ private $db_events = 'events';
+ private $db_calendars = 'calendars';
+ private $db_attachments = 'attachments';
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->server_timezone = new DateTimeZone(date_default_timezone_get());
+
+ // read database config
+ $db = $this->rc->get_dbh();
+ $this->db_events = $this->rc->config->get('db_table_events', $db->table_name($this->db_events));
+ $this->db_calendars = $this->rc->config->get('db_table_calendars', $db->table_name($this->db_calendars));
+ $this->db_attachments = $this->rc->config->get('db_table_attachments', $db->table_name($this->db_attachments));
+
+ $this->_read_calendars();
+ }
+
+ /**
+ * Read available calendars for the current user and store them internally
+ */
+ protected function _read_calendars()
+ {
+ $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', '')));
+
+ if (!empty($this->rc->user->ID)) {
+ $calendar_ids = array();
+ $result = $this->rc->db->query(
+ "SELECT *, calendar_id AS id FROM " . $this->db_calendars . "
+ WHERE user_id=?
+ ORDER BY name",
+ $this->rc->user->ID
+ );
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $arr['showalarms'] = intval($arr['showalarms']);
+ $arr['active'] = !in_array($arr['id'], $hidden);
+ $arr['name'] = html::quote($arr['name']);
+ $arr['listname'] = html::quote($arr['name']);
+ $arr['rights'] = 'lrswikxteav';
+ $arr['editable'] = true;
+ $this->calendars[$arr['calendar_id']] = $arr;
+ $calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
+ }
+ $this->calendar_ids = join(',', $calendar_ids);
+ }
+ }
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer Bitmask defining filter criterias
+ *
+ * @return array List of calendars
+ */
+ public function list_calendars($filter = 0)
+ {
+ // attempt to create a default calendar for this user
+ if (empty($this->calendars) && get_class($this) == "database_driver") {
+ if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000', 'showalarms' => true)))
+ $this->_read_calendars();
+ }
+
+ $calendars = $this->calendars;
+
+ // filter active calendars
+ if ($filter & self::FILTER_ACTIVE) {
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ }
+
+ // 'personal' is unsupported in this driver
+
+ // append the virtual birthdays calendar
+ if ($this->rc->config->get('calendar_contact_birthdays', false)) {
+ $prefs = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA'));
+ $hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', '')));
+
+ $id = self::BIRTHDAY_CALENDAR_ID;
+ if (!$active || !in_array($id, $hidden)) {
+ $calendars[$id] = array(
+ 'id' => $id,
+ 'name' => $this->cal->gettext('birthdays'),
+ 'listname' => $this->cal->gettext('birthdays'),
+ 'color' => $prefs['color'],
+ 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
+ 'active' => !in_array($id, $hidden),
+ 'group' => 'x-birthdays',
+ 'editable' => false,
+ 'default' => false,
+ 'children' => false,
+ );
+ }
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * @return mixed ID of the calendar on success, False on error
+ */
+ public function create_calendar($prop)
+ {
+ $result = $this->rc->db->query(
+ "INSERT INTO " . $this->db_calendars . "
+ (user_id, name, color, showalarms)
+ VALUES (?, ?, ?, ?)",
+ $this->rc->user->ID,
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms']?1:0
+ );
+
+ if ($result)
+ return $this->rc->db->insert_id($this->db_calendars);
+
+ return false;
+ }
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function edit_calendar($prop)
+ {
+ // birthday calendar properties are saved in user prefs
+ if ($prop['id'] == self::BIRTHDAY_CALENDAR_ID) {
+ $prefs['birthday_calendar'] = $this->rc->config->get('birthday_calendar', array('color' => '87CEFA'));
+ if (isset($prop['color']))
+ $prefs['birthday_calendar']['color'] = $prop['color'];
+ if (isset($prop['showalarms']))
+ $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
+ $this->rc->user->save_prefs($prefs);
+ return true;
+ }
+
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_calendars . "
+ SET name=?, color=?, showalarms=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms']?1:0,
+ $prop['id'],
+ $this->rc->user->ID
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Set active/subscribed state of a calendar
+ * Save a list of hidden calendars in user prefs
+ *
+ * @see calendar_driver::subscribe_calendar()
+ */
+ public function subscribe_calendar($prop)
+ {
+ $hidden = array_flip(explode(',', $this->rc->config->get('hidden_calendars', '')));
+
+ if ($prop['active'])
+ unset($hidden[$prop['id']]);
+ else
+ $hidden[$prop['id']] = 1;
+
+ return $this->rc->user->save_prefs(array('hidden_calendars' => join(',', array_keys($hidden))));
+ }
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @see calendar_driver::delete_calendar()
+ */
+ public function delete_calendar($prop)
+ {
+ if (!$this->calendars[$prop['id']])
+ return false;
+
+ // events and attachments will be deleted by foreign key cascade
+
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_calendars . "
+ WHERE calendar_id=?",
+ $prop['id']
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Add a single event to the database
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::new_event()
+ */
+ public function new_event($event)
+ {
+ if (!$this->validate($event))
+ return false;
+
+ if (!empty($this->calendars)) {
+ if ($event['calendar'] && !$this->calendars[$event['calendar']])
+ return false;
+ if (!$event['calendar'])
+ $event['calendar'] = reset(array_keys($this->calendars));
+
+ if ($event_id = $this->_insert_event($event)) {
+ $this->_update_recurring($event);
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ *
+ */
+ private function _insert_event(&$event)
+ {
+ $event = $this->_save_preprocess($event);
+
+ $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence,
+ title, description, location, categories, url, free_busy, priority, sensitivity, status, attendees, alarms, notifyat)
+ VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['calendar'],
+ strval($event['uid']),
+ intval($event['recurrence_id']),
+ strval($event['_instance']),
+ intval($event['isexception']),
+ $event['start']->format(self::DB_DATE_FORMAT),
+ $event['end']->format(self::DB_DATE_FORMAT),
+ intval($event['all_day']),
+ $event['_recurrence'],
+ strval($event['title']),
+ strval($event['description']),
+ strval($event['location']),
+ join(',', (array)$event['categories']),
+ strval($event['url']),
+ intval($event['free_busy']),
+ intval($event['priority']),
+ intval($event['sensitivity']),
+ strval($event['status']),
+ $event['attendees'],
+ $event['alarms'],
+ $event['notifyat']
+ );
+
+ $event_id = $this->rc->db->insert_id($this->db_events);
+
+ if ($event_id) {
+ $event['id'] = $event_id;
+
+ // add attachments
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event_id);
+ unset($attachment);
+ }
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::edit_event()
+ */
+ public function edit_event($event)
+ {
+ if (!empty($this->calendars)) {
+ $update_master = false;
+ $update_recurring = true;
+ $old = $this->get_event($event);
+ $ret = true;
+
+ // check if update affects scheduling and update attendee status accordingly
+ $reschedule = $this->_check_scheduling($event, $old, true);
+
+ // increment sequence number
+ if (empty($event['sequence']) && $reschedule)
+ $event['sequence'] = max($event['sequence'], $old['sequence']) + 1;
+
+ // modify a recurring event, check submitted savemode to do the right things
+ if ($old['recurrence'] || $old['recurrence_id']) {
+ $master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
+
+ // keep saved exceptions (not submitted by the client)
+ if ($old['recurrence']['EXDATE'])
+ $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
+
+ switch ($event['_savemode']) {
+ case 'new':
+ $event['uid'] = $this->cal->generate_uid();
+ return $this->new_event($event);
+
+ case 'current':
+ // save as exception
+ $event['isexception'] = 1;
+ $update_recurring = false;
+
+ // set exception to first instance (= master)
+ if ($event['id'] == $master['id']) {
+ $event += $old;
+ $event['recurrence_id'] = $master['id'];
+ $event['_instance'] = libcalendaring::recurrence_instance_identifier($old);
+ $event['isexception'] = 1;
+ $event_id = $this->_insert_event($event);
+ return $event_id;
+ }
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event, then save this instance as new recurring event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // if recurrence COUNT, update value to the correct number of future occurences
+ if ($event['recurrence']['COUNT']) {
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $sqlresult = $this->rc->db->query(sprintf(
+ "SELECT event_id FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND %s >= ?
+ AND recurrence_id=?",
+ $this->calendar_ids,
+ $this->rc->db->quote_identifier('start')
+ ),
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']);
+ if ($count = $this->rc->db->num_rows($sqlresult))
+ $event['recurrence']['COUNT'] = $count;
+ }
+
+ $update_recurring = true;
+ $event['recurrence_id'] = 0;
+ $event['isexception'] = 0;
+ $event['_instance'] = '';
+ break;
+ }
+ // else: 'future' == 'all' if modifying the master event
+
+ default: // 'all' is default
+ $event['id'] = $master['id'];
+ $event['recurrence_id'] = 0;
+
+ // use start date from master but try to be smart on time or duration changes
+ $old_start_date = $old['start']->format('Y-m-d');
+ $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
+ $old_duration = $old['end']->format('U') - $old['start']->format('U');
+
+ $new_start_date = $event['start']->format('Y-m-d');
+ $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
+ $new_duration = $event['end']->format('U') - $event['start']->format('U');
+
+ $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
+
+ // shifted or resized
+ if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
+ $event['start'] = $master['start']->add($old['start']->diff($event['start']));
+ $event['end'] = clone $event['start'];
+ $event['end']->add(new DateInterval('PT'.$new_duration.'S'));
+ }
+ // dates did not change, use the ones from master
+ else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
+ $event['start'] = $master['start'];
+ $event['end'] = $master['end'];
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if (is_array($event['recurrence']) && ($old_start_date != $new_start_date || $old_start_time != $new_start_time)
+ && ($exceptions = $this->_load_exceptions($old))) {
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ foreach ($exceptions as $exception) {
+ $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ if (is_a($recurrence_id, 'DateTime')) {
+ $recurrence_id->add($date_shift);
+ $exception['_instance'] = $recurrence_id->format($recurrence_id_format);
+ $this->_update_event($exception, false);
+ }
+ }
+ }
+
+ $ret = $event['id']; // return master ID
+ break;
+ }
+ }
+
+ $success = $this->_update_event($event, $update_recurring);
+
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
+ $update_event = $this->get_event(array('id' => $event['recurrence_id']));
+ $update_event['_savemode'] = $event['_savemode'];
+ calendar::merge_attendee_data($update_event, $attendees);
+ }
+
+ if ($ret = $this->update_attendees($update_event, $attendees)) {
+ // replace $event with effectively updated event (for iTip reply)
+ if ($ret !== true && $ret != $update_event['id'] && ($new_event = $this->get_event(array('id' => $ret)))) {
+ $event = $new_event;
+ }
+ else {
+ $event = $update_event;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ $success = $this->edit_event($event, true);
+
+ // apply attendee updates to recurrence exceptions too
+ if ($success && $event['_savemode'] == 'all' && !empty($event['recurrence']) && empty($event['recurrence_id']) && ($exceptions = $this->_load_exceptions($event))) {
+ foreach ($exceptions as $exception) {
+ calendar::merge_attendee_data($exception, $attendees);
+ $this->_update_event($exception, false);
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Determine whether the current change affects scheduling and reset attendee status accordingly
+ */
+ private function _check_scheduling(&$event, $old, $update = true)
+ {
+ // skip this check when importing iCal/iTip events
+ if (isset($event['sequence']) || !empty($event['_method'])) {
+ return false;
+ }
+
+ $reschedule = false;
+
+ // iterate through the list of properties considered 'significant' for scheduling
+ foreach (self::$scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $event[$prop];
+ if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
+ unset($a['EXCEPTIONS'], $b['EXCEPTIONS']);
+ $a = array_filter($a);
+ $b = array_filter($b);
+
+ // advanced rrule comparison: no rescheduling if series was shortened
+ if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
+ unset($a['COUNT'], $b['COUNT']);
+ }
+ else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
+ unset($a['UNTIL'], $b['UNTIL']);
+ }
+ }
+ if ($a != $b) {
+ $reschedule = true;
+ break;
+ }
+ }
+
+ // reset all attendee status to needs-action (#4360)
+ if ($update && $reschedule && is_array($event['attendees'])) {
+ $is_organizer = false;
+ $emails = $this->cal->get_user_emails();
+ $attendees = $event['attendees'];
+ foreach ($attendees as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $is_organizer = true;
+ }
+ else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
+ $attendees[$i]['status'] = 'NEEDS-ACTION';
+ $attendees[$i]['rsvp'] = true;
+ }
+ }
+
+ // update attendees only if I'm the organizer
+ if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
+ $event['attendees'] = $attendees;
+ }
+ }
+
+ return $reschedule;
+ }
+
+ /**
+ * Convert save data to be used in SQL statements
+ */
+ private function _save_preprocess($event)
+ {
+ // shift dates to server's timezone (except for all-day events)
+ if (!$event['allday']) {
+ $event['start'] = clone $event['start'];
+ $event['start']->setTimezone($this->server_timezone);
+ $event['end'] = clone $event['end'];
+ $event['end']->setTimezone($this->server_timezone);
+ }
+
+ // compose vcalendar-style recurrencue rule from structured data
+ $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
+ $event['_recurrence'] = rtrim($rrule, ';');
+ $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
+ $event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]);
+
+ if ($event['free_busy'] == 'tentative') {
+ $event['status'] = 'TENTATIVE';
+ }
+
+ if (isset($event['allday'])) {
+ $event['all_day'] = $event['allday'] ? 1 : 0;
+ }
+
+ // compute absolute time to notify the user
+ $event['notifyat'] = $this->_get_notification($event);
+
+ if (is_array($event['valarms'])) {
+ $event['alarms'] = $this->serialize_alarms($event['valarms']);
+ }
+
+ // process event attendees
+ if (!empty($event['attendees']))
+ $event['attendees'] = json_encode((array)$event['attendees']);
+ else
+ $event['attendees'] = '';
+
+ return $event;
+ }
+
+ /**
+ * Compute absolute time to notify the user
+ */
+ private function _get_notification($event)
+ {
+ if ($event['valarms'] && $event['start'] > new DateTime()) {
+ $alarm = libcalendaring::get_next_alarm($event);
+
+ if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
+ return date('Y-m-d H:i:s', $alarm['time']);
+ }
+
+ return null;
+ }
+
+ /**
+ * Save the given event record to database
+ *
+ * @param array Event data
+ * @param boolean True if recurring events instances should be updated, too
+ */
+ private function _update_event($event, $update_recurring = true)
+ {
+ $event = $this->_save_preprocess($event);
+ $sql_set = array();
+ $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat');
+ foreach ($set_cols as $col) {
+ if (is_object($event[$col]) && is_a($event[$col], 'DateTime'))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT));
+ else if (is_array($event[$col]))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event[$col]));
+ else if (array_key_exists($col, $event))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
+ }
+
+ if ($event['_recurrence'])
+ $sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']);
+
+ if ($event['_instance'])
+ $sql_set[] = $this->rc->db->quote_identifier('instance') . '=' . $this->rc->db->quote($event['_instance']);
+
+ if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar'])
+ $sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']);
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s %s
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now(),
+ ($sql_set ? ', ' . join(', ', $sql_set) : '')
+ ),
+ $event['id']
+ );
+
+ $success = $this->rc->db->affected_rows($query);
+
+ // add attachments
+ if ($success && !empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event['id']);
+ unset($attachment);
+ }
+ }
+
+ // remove attachments
+ if ($success && !empty($event['deleted_attachments'])) {
+ foreach ($event['deleted_attachments'] as $attachment) {
+ $this->remove_attachment($attachment, $event['id']);
+ }
+ }
+
+ if ($success) {
+ unset($this->cache[$event['id']]);
+ if ($update_recurring)
+ $this->_update_recurring($event);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Insert "fake" entries for recurring occurences of this event
+ */
+ private function _update_recurring($event)
+ {
+ if (empty($this->calendars))
+ return;
+
+ if (!empty($event['recurrence'])) {
+ $exdata = array();
+ $exceptions = $this->_load_exceptions($event);
+
+ foreach ($exceptions as $exception) {
+ $exdate = substr($exception['_instance'], 0, 8);
+ $exdata[$exdate] = $exception;
+ }
+ }
+
+ // clear existing recurrence copies
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE recurrence_id=?
+ AND isexception=0
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $event['id']
+ );
+
+ // create new fake entries
+ if (!empty($event['recurrence'])) {
+ // include library class
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+
+ $recurrence = new calendar_recurrence($this->cal, $event);
+
+ $count = 0;
+ $event['allday'] = $event['all_day'];
+ $duration = $event['start']->diff($event['end']);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ while ($next_start = $recurrence->next_start()) {
+ $instance = $next_start->format($recurrence_id_format);
+ $datestr = substr($instance, 0, 8);
+
+ // skip exceptions
+ // TODO: merge updated data from master event
+ if ($exdata[$datestr]) {
+ continue;
+ }
+
+ $next_start->setTimezone($this->server_timezone);
+ $next_end = clone $next_start;
+ $next_end->add($duration);
+
+ $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end, 'status' => $event['status']));
+ $query = $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat)
+ SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ?
+ FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['id'],
+ $instance,
+ $next_start->format(self::DB_DATE_FORMAT),
+ $next_end->format(self::DB_DATE_FORMAT),
+ $notify_at,
+ $event['id']
+ );
+
+ if (!$this->rc->db->affected_rows($query))
+ break;
+
+ // stop adding events for inifinite recurrence after 20 years
+ if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20))
+ break;
+ }
+
+ // remove all exceptions after recurrence end
+ if ($next_end && !empty($exceptions)) {
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `start` > ?
+ AND `calendar_id` IN (" . $this->calendar_ids . ")",
+ $event['id'],
+ $next_end->format(self::DB_DATE_FORMAT)
+ );
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ private function _load_exceptions($event, $instance_id = null)
+ {
+ $sql_add_where = '';
+ if (!empty($instance_id)) {
+ $sql_add_where = 'AND `instance`=?';
+ }
+
+ $result = $this->rc->db->query(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `calendar_id` IN (" . $this->calendar_ids . ")
+ $sql_add_where
+ ORDER BY `instance`, `start`",
+ $event['id'],
+ $instance_id
+ );
+
+ $exceptions = array();
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $exception = $this->_read_postprocess($sql_arr);
+ $instance = $exception['_instance'] ?: $exception['start']->format($exception['allday'] ? 'Ymd' : 'Ymd\THis');
+ $exceptions[$instance] = $exception;
+ }
+
+ return $exceptions;
+ }
+
+ /**
+ * Move a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::move_event()
+ */
+ public function move_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Resize a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::resize_event()
+ */
+ public function resize_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Remove a single event from the database
+ *
+ * @param array Hash array with event properties
+ * @param boolean Remove record irreversible (@TODO)
+ *
+ * @see calendar_driver::remove_event()
+ */
+ public function remove_event($event, $force = true)
+ {
+ if (!empty($this->calendars)) {
+ $event += (array)$this->get_event($event);
+ $master = $event;
+ $update_master = false;
+ $savemode = 'all';
+ $ret = true;
+
+ // read master if deleting a recurring event
+ if ($event['recurrence'] || $event['recurrence_id']) {
+ $master = $event['recurrence_id'] ? $this->get_event(array('id' => $event['recurrence_id'])) : $event;
+ $savemode = $event['_savemode'];
+ }
+
+ switch ($savemode) {
+ case 'current':
+ // add exception to master event
+ $master['recurrence']['EXDATE'][] = $event['start'];
+ $update_master = true;
+
+ // just delete this single occurence
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND event_id=?",
+ $event['id']
+ );
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // delete this and all future instances
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND " . $this->rc->db->quote_identifier('start') . " >= ?
+ AND recurrence_id=?",
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']
+ );
+ $ret = $master['id'];
+ break;
+ }
+ // else: future == all if modifying the master event
+
+ default: // 'all' is default
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE (event_id=? OR recurrence_id=?)
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $master['id'],
+ $master['id']
+ );
+ break;
+ }
+
+ $success = $this->rc->db->affected_rows($query);
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return data of a specific event
+ * @param mixed Hash array with event properties or event UID
+ * @param integer Bitmask defining the scope to search events in
+ * @param boolean If true, recurrence exceptions shall be added
+ * @return array Hash array with event properties
+ */
+ public function get_event($event, $scope = 0, $full = false)
+ {
+ $id = is_array($event) ? ($event['id'] ?: $event['uid']) : $event;
+ $cal = is_array($event) ? $event['calendar'] : null;
+ $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid';
+
+ $where_add = '';
+ if (is_array($event) && !$event['id'] && !empty($event['_instance'])) {
+ $where_add = 'AND instance=' . $this->rc->db->quote($event['_instance']);
+ }
+
+ if ($this->cache[$id])
+ return $this->cache[$id];
+
+ // get event from the address books birthday calendar
+ if ($cal == self::BIRTHDAY_CALENDAR_ID) {
+ return $this->get_birthday_event($id);
+ }
+
+ if ($scope & self::FILTER_ACTIVE) {
+ $calendars = $this->calendars;
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ $cals = join(',', $calendars);
+ }
+ else {
+ $cals = $this->calendar_ids;
+ }
+
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " AS e
+ WHERE e.calendar_id IN (%s)
+ AND e.$col=?
+ %s",
+ $cals,
+ $where_add
+ ),
+ $id);
+
+ if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $event = $this->_read_postprocess($sql_arr);
+
+ // also load recurrence exceptions
+ if (!empty($event['recurrence']) && $full) {
+ $event['recurrence']['EXCEPTIONS'] = array_values($this->_load_exceptions($event));
+ }
+
+ $this->cache[$id] = $event;
+ return $this->cache[$id];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars)));
+
+ // compose (slow) SQL query for searching
+ // FIXME: improve searching using a dedicated col and normalized values
+ if ($query) {
+ foreach (array('title','location','description','categories','attendees') as $col)
+ $sql_query[] = $this->rc->db->ilike($col, '%'.$query.'%');
+ $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
+ }
+
+ if (!$virtual)
+ $sql_add .= ' AND e.recurrence_id = 0';
+
+ if ($modifiedsince)
+ $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince));
+
+ $events = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " e
+ WHERE e.calendar_id IN (%s)
+ AND e.start <= %s AND e.end >= %s
+ %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($end),
+ $this->rc->db->fromunixtime($start),
+ $sql_add
+ ));
+
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) {
+ $event = $this->_read_postprocess($sql_arr);
+ $add = true;
+
+ if (!empty($event['recurrence']) && !$event['recurrence_id']) {
+ // load recurrence exceptions (i.e. for export)
+ if (!$virtual) {
+ $event['recurrence']['EXCEPTIONS'] = $this->_load_exceptions($event);
+ }
+ // check for exception on first instance
+ else {
+ $instance = libcalendaring::recurrence_instance_identifier($event);
+ $exceptions = $this->_load_exceptions($event, $instance);
+ if ($exceptions && is_array($exceptions[$instance])) {
+ $event = $exceptions[$instance];
+ $add = false;
+ }
+ }
+ }
+
+ if ($add)
+ $events[] = $event;
+ }
+ }
+
+ // add events from the address books birthday calendar
+ if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars) && empty($query)) {
+ $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ public function count_events($calendars, $start, $end = null)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Convert sql record into a rcube style event object
+ */
+ private function _read_postprocess($event)
+ {
+ $free_busy_map = array_flip($this->free_busy_map);
+ $sensitivity_map = array_flip($this->sensitivity_map);
+
+ $event['id'] = $event['event_id'];
+ $event['start'] = new DateTime($event['start']);
+ $event['end'] = new DateTime($event['end']);
+ $event['allday'] = intval($event['all_day']);
+ $event['created'] = new DateTime($event['created']);
+ $event['changed'] = new DateTime($event['changed']);
+ $event['free_busy'] = $free_busy_map[$event['free_busy']];
+ $event['sensitivity'] = $sensitivity_map[$event['sensitivity']];
+ $event['calendar'] = $event['calendar_id'];
+ $event['recurrence_id'] = intval($event['recurrence_id']);
+ $event['isexception'] = intval($event['isexception']);
+
+ // parse recurrence rule
+ if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
+ $event['recurrence'] = array();
+ foreach ($m as $rr) {
+ if (is_numeric($rr[2]))
+ $rr[2] = intval($rr[2]);
+ else if ($rr[1] == 'UNTIL')
+ $rr[2] = date_create($rr[2]);
+ else if ($rr[1] == 'RDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ else if ($rr[1] == 'EXDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ $event['recurrence'][$rr[1]] = $rr[2];
+ }
+ }
+
+ if ($event['recurrence_id']) {
+ libcalendaring::identify_recurrence_instance($event);
+ }
+
+ if (strlen($event['instance'])) {
+ $event['_instance'] = $event['instance'];
+
+ if (empty($event['recurrence_id'])) {
+ $event['recurrence_date'] = rcube_utils::anytodatetime($event['_instance'], $event['start']->getTimezone());
+ }
+ }
+
+ if ($event['_attachments'] > 0) {
+ $event['attachments'] = (array)$this->list_attachments($event);
+ }
+
+ // decode serialized event attendees
+ if (strlen($event['attendees'])) {
+ $event['attendees'] = $this->unserialize_attendees($event['attendees']);
+ }
+ else {
+ $event['attendees'] = array();
+ }
+
+ // decode serialized alarms
+ if ($event['alarms']) {
+ $event['valarms'] = $this->unserialize_alarms($event['alarms']);
+ }
+
+ unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['instance'], $event['_attachments']);
+ return $event;
+ }
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @see calendar_driver::pending_alarms()
+ */
+ public function pending_alarms($time, $calendars = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (is_string($calendars))
+ $calendars = explode(',', $calendars);
+
+ // only allow to select from calendars with activated alarms
+ $calendar_ids = array();
+ foreach ($calendars as $cid) {
+ if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms'])
+ $calendar_ids[] = $cid;
+ }
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids);
+
+ $alarms = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND notifyat <= %s AND %s > %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($time),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->fromunixtime($time)
+ ));
+
+ while ($result && ($event = $this->rc->db->fetch_assoc($result)))
+ $alarms[] = $this->_read_postprocess($event);
+ }
+
+ return $alarms;
+ }
+
+ /**
+ * Feedback after showing/sending an alarm notification
+ *
+ * @see calendar_driver::dismiss_alarm()
+ */
+ public function dismiss_alarm($event_id, $snooze = 0)
+ {
+ // set new notifyat time or unset if not snoozed
+ $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null;
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s, notifyat=?
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now()),
+ $notify_at,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Save an attachment related to the given event
+ */
+ private function add_attachment($attachment, $event_id)
+ {
+ $data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
+
+ $query = $this->rc->db->query(
+ "INSERT INTO " . $this->db_attachments .
+ " (event_id, filename, mimetype, size, data)" .
+ " VALUES (?, ?, ?, ?, ?)",
+ $event_id,
+ $attachment['name'],
+ $attachment['mimetype'],
+ strlen($data),
+ base64_encode($data)
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Remove a specific attachment from the given event
+ */
+ private function remove_attachment($attachment_id, $event_id)
+ {
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_attachments .
+ " WHERE attachment_id = ?" .
+ " AND event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id = ?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))",
+ $attachment_id,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * List attachments of specified event
+ */
+ public function list_attachments($event)
+ {
+ $attachments = array();
+
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id=?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))".
+ " ORDER BY filename",
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id']
+ );
+
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $attachments[] = $arr;
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?",
+ $id,
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return $arr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get attachment body
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT data " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?".
+ " AND event_id=?",
+ $id,
+ $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return base64_decode($arr['data']);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=''
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=?
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name,
+ $oldname
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Helper method to serialize the list of alarms into a string
+ */
+ private function serialize_alarms($valarms)
+ {
+ foreach ((array)$valarms as $i => $alarm) {
+ if ($alarm['trigger'] instanceof DateTime) {
+ $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c');
+ }
+ }
+
+ return $valarms ? json_encode($valarms) : null;
+ }
+
+ /**
+ * Helper method to decode a serialized list of alarms
+ */
+ private function unserialize_alarms($alarms)
+ {
+ // decode json serialized alarms
+ if ($alarms && $alarms[0] == '[') {
+ $valarms = json_decode($alarms, true);
+ foreach ($valarms as $i => $alarm) {
+ if ($alarm['trigger'][0] == '@') {
+ try {
+ $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1));
+ }
+ catch (Exception $e) {
+ unset($valarms[$i]);
+ }
+ }
+ }
+ }
+ // convert legacy alarms data
+ else if (strlen($alarms)) {
+ list($trigger, $action) = explode(':', $alarms, 2);
+ if ($trigger = libcalendaring::parse_alarm_value($trigger)) {
+ $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
+ }
+ }
+
+ return $valarms;
+ }
+
+ /**
+ * Helper method to decode the attendees list from string
+ */
+ private function unserialize_attendees($s_attendees)
+ {
+ $attendees = array();
+
+ // decode json serialized string
+ if ($s_attendees[0] == '[') {
+ $attendees = json_decode($s_attendees, true);
+ }
+ // decode the old serialization format
+ else {
+ foreach (explode("\n", $event['attendees']) as $line) {
+ $att = array();
+ foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) {
+ list($key, $value) = explode("=", $prop);
+ $att[strtolower($key)] = stripslashes(trim($value, '""'));
+ }
+ $attendees[] = $att;
+ }
+ }
+
+ return $attendees;
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ $db = $this->rc->db;
+ $user = $args['user'];
+ $event_ids = array();
+
+ $events = $db->query(
+ "SELECT event_id FROM " . $this->db_events . " AS ev" .
+ " LEFT JOIN " . $this->db_calendars . " cal ON (ev.calendar_id = cal.calendar_id)".
+ " WHERE user_id=?",
+ $user->ID);
+
+ while ($row = $db->fetch_assoc($events)) {
+ $event_ids[] = $row['event_id'];
+ }
+
+ if (!empty($event_ids)) {
+ foreach (array($this->db_attachments, $this->db_events) as $table) {
+ $db->query(sprintf("DELETE FROM $table WHERE event_id IN (%s)", join(',', $event_ids)));
+ }
+ }
+
+ foreach (array($this->db_calendars, 'itipinvitations') as $table) {
+ $db->query("DELETE FROM $table WHERE user_id=?", $user->ID);
+ }
+ }
+
+}
diff --git a/calendar/drivers/ical/SQL/mysql.initial.sql b/calendar/drivers/ical/SQL/mysql.initial.sql
new file mode 100644
index 0000000..6ae4dec
--- /dev/null
+++ b/calendar/drivers/ical/SQL/mysql.initial.sql
@@ -0,0 +1,91 @@
+/**
+ * iCAL Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+CREATE TABLE IF NOT EXISTS `ical_calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+
+ `ical_url` varchar(255) NOT NULL,
+ `ical_user` varchar(255) DEFAULT NULL,
+ `ical_pass` varchar(1024) DEFAULT NULL,
+ `ical_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`calendar_id`),
+ INDEX `ical_user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_ical_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `ical_events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `instance` varchar(16) NOT NULL DEFAULT '',
+ `isexception` tinyint(1) NOT NULL DEFAULT '0',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` text NULL DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+
+ `ical_url` varchar(255) NOT NULL,
+ `ical_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`event_id`),
+ INDEX `ical_uid_idx` (`uid`),
+ INDEX `ical_recurrence_idx` (`recurrence_id`),
+ INDEX `ical_calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_ical_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `ical_calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `ical_attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_ical_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `ical_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-ical-version', '2015022700'); \ No newline at end of file
diff --git a/calendar/drivers/ical/SQL/mysql/.keep_dir b/calendar/drivers/ical/SQL/mysql/.keep_dir
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/calendar/drivers/ical/SQL/mysql/.keep_dir
diff --git a/calendar/drivers/ical/SQL/mysql/2015022500.sql b/calendar/drivers/ical/SQL/mysql/2015022500.sql
new file mode 100644
index 0000000..6dc8727
--- /dev/null
+++ b/calendar/drivers/ical/SQL/mysql/2015022500.sql
@@ -0,0 +1,124 @@
+/**
+ * iCAL Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Create new tables */
+CREATE TABLE IF NOT EXISTS `ical_calendars` (
+ `calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `name` varchar(255) NOT NULL,
+ `color` varchar(8) NOT NULL,
+ `showalarms` tinyint(1) NOT NULL DEFAULT '1',
+
+ `ical_url` varchar(255) NOT NULL,
+ `ical_user` varchar(255) DEFAULT NULL,
+ `ical_pass` varchar(1024) DEFAULT NULL,
+ `ical_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+
+ PRIMARY KEY(`calendar_id`),
+ INDEX `ical_user_name_idx` (`user_id`, `name`),
+ CONSTRAINT `fk_ical_calendars_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `ical_events` (
+ `event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `uid` varchar(255) NOT NULL DEFAULT '',
+ `created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
+ `start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
+ `recurrence` varchar(255) DEFAULT NULL,
+ `title` varchar(255) NOT NULL,
+ `description` text NOT NULL,
+ `location` varchar(255) NOT NULL DEFAULT '',
+ `categories` varchar(255) NOT NULL DEFAULT '',
+ `url` varchar(255) NOT NULL DEFAULT '',
+ `all_day` tinyint(1) NOT NULL DEFAULT '0',
+ `free_busy` tinyint(1) NOT NULL DEFAULT '0',
+ `priority` tinyint(1) NOT NULL DEFAULT '0',
+ `sensitivity` tinyint(1) NOT NULL DEFAULT '0',
+ `status` varchar(32) NOT NULL DEFAULT '',
+ `alarms` varchar(255) DEFAULT NULL,
+ `attendees` text DEFAULT NULL,
+ `notifyat` datetime DEFAULT NULL,
+
+ PRIMARY KEY(`event_id`),
+ INDEX `ical_uid_idx` (`uid`),
+ INDEX `ical_recurrence_idx` (`recurrence_id`),
+ INDEX `ical_calendar_notify_idx` (`calendar_id`,`notifyat`),
+ CONSTRAINT `fk_ical_events_calendar_id` FOREIGN KEY (`calendar_id`)
+ REFERENCES `calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE IF NOT EXISTS `ical_attachments` (
+ `attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
+ `filename` varchar(255) NOT NULL DEFAULT '',
+ `mimetype` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) NOT NULL DEFAULT '0',
+ `data` longtext NOT NULL,
+ PRIMARY KEY(`attachment_id`),
+ CONSTRAINT `fk_ical_attachments_event_id` FOREIGN KEY (`event_id`)
+ REFERENCES `events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+/* Migrate Data */
+INSERT INTO ical_calendars
+ SELECT calendar_id, user_id, `name`, color, showalarms,
+ url as ical_url, NULL as ical_user, NULL as ical_pass, last_change as ical_last_change
+ FROM calendars cal, ical_props dav
+ WHERE dav.obj_id = cal.calendar_id
+ AND dav.obj_type = 'ical';
+
+INSERT INTO ical_events SELECT e.* FROM `events` e
+ WHERE e.calendar_id IN (
+ SELECT obj_id FROM ical_props
+ WHERE obj_type = 'ical'
+ );
+
+INSERT INTO ical_attachments SELECT * FROM attachments a
+WHERE a.event_id IN (
+ SELECT e.event_id FROM `events` e
+ WHERE e.calendar_id IN (
+ SELECT obj_id FROM ical_props
+ WHERE obj_type = 'ical'
+ )
+);
+
+/* Drop deprecated data */
+DELETE FROM `events` WHERE event_id IN (
+ SELECT obj_id FROM ical_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+DELETE FROM calendars WHERE calendar_id IN (
+ SELECT obj_id FROM ical_props dav
+ WHERE dav.obj_type = 'ical'
+);
+DELETE FROM attachments WHERE event_id IN (
+ SELECT obj_id FROM ical_props dav
+ WHERE dav.obj_type = 'vevent'
+);
+DROP TABLE ical_props;
+
diff --git a/calendar/drivers/ical/SQL/mysql/2015022700.sql b/calendar/drivers/ical/SQL/mysql/2015022700.sql
new file mode 100644
index 0000000..3acb892
--- /dev/null
+++ b/calendar/drivers/ical/SQL/mysql/2015022700.sql
@@ -0,0 +1,14 @@
+-- add identifier for recurring instances and exceptions
+
+ALTER TABLE `ical_events` ADD `instance` varchar(16) NOT NULL DEFAULT '' AFTER `uid`;
+ALTER TABLE `ical_events` ADD `isexception` tinyint(1) NOT NULL DEFAULT '0' AFTER `instance`;
+
+UPDATE `ical_events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%d')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 1;
+
+UPDATE `ical_events` SET `instance` = DATE_FORMAT(`start`, '%Y%m%dT%k%i%s')
+ WHERE `recurrence_id` != 0 AND `instance` = '' AND `all_day` = 0;
+
+-- extend alarms columns for multiple values
+
+ALTER TABLE `ical_events` CHANGE `alarms` `alarms` TEXT NULL DEFAULT NULL; \ No newline at end of file
diff --git a/calendar/drivers/ical/ical_driver.php b/calendar/drivers/ical/ical_driver.php
new file mode 100644
index 0000000..0916a11
--- /dev/null
+++ b/calendar/drivers/ical/ical_driver.php
@@ -0,0 +1,1821 @@
+<?php
+
+/**
+ * iCalendar driver for the Calendar plugin
+ *
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once(dirname(__FILE__) . '/ical_sync.php');
+require_once (dirname(__FILE__).'/../../lib/encryption.php');
+
+/**
+ * TODO
+ * - Postgresql, Sqlite scripts.
+ *
+ */
+class ical_driver extends calendar_driver
+{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
+ public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'cancelled');
+
+ // features this backend supports
+ public $alarms = true;
+ public $attendees = true;
+ public $freebusy = false;
+ public $attachments = true;
+ public $alarm_types = array('DISPLAY');
+
+ private $rc;
+ private $cal;
+ private $cache = array();
+ private $calendars = array();
+ private $calendar_ids = '';
+ private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
+ private $sensitivity_map = array('public' => 0, 'private' => 1, 'confidential' => 2);
+ private $server_timezone;
+
+ private $db_events = 'ical_events';
+ private $db_calendars = 'ical_calendars';
+ private $db_attachments = 'ical_attachments';
+
+
+ private $sync_clients = array();
+
+ // Min. time period to wait until sync check.
+ private $sync_period = 10; // TODO: 600; // seconds
+
+ // Crypt key for CalDAV auth
+ private $crypt_key;
+
+ // Indicates debug mode for iCAL
+ static private $debug = null;
+
+ /**
+ * Helper method to log debug msg if debug mode is enabled.
+ */
+ static public function debug_log($msg)
+ {
+ if(self::$debug === true)
+ rcmail::console(__CLASS__.': '.$msg);
+ }
+
+ /**
+ * Helper method to log (if debug mode is enabled) and raise an user error.
+ */
+ private function _raise_error($msg)
+ {
+ self::debug_log($msg);
+ $this->rc->output->show_message($msg, 'error');
+ }
+
+ /**
+ * Default constructor
+ */
+ public function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->server_timezone = new DateTimeZone(date_default_timezone_get());
+
+ // read database config
+ $db = $this->rc->get_dbh();
+ $this->db_events = $this->rc->config->get('db_table_events', $db->table_name($this->db_events));
+ $this->db_calendars = $this->rc->config->get('db_table_calendars', $db->table_name($this->db_calendars));
+ $this->db_attachments = $this->rc->config->get('db_table_attachments', $db->table_name($this->db_attachments));
+ $this->crypt_key = $this->rc->config->get("calendar_crypt_key", "%E`c{2;<J2F^4_&._BxfQ<5Pf3qv!m{e");
+
+ // Set debug state
+ if (self::$debug === null)
+ self::$debug = $this->rc->config->get('calendar_ical_debug', false);
+
+ // PHP's fopen wrappers must be allowed
+ if(!ini_get("allow_url_fopen"))
+ self::_raise_error("iCAL driver needs PHP's fopen wrappers to be allowed!");
+
+ $this->_read_calendars();
+ }
+
+ /**
+ * Read available calendars for the current user and store them internally
+ */
+ protected function _read_calendars()
+ {
+ $hidden = array_filter(explode(',', $this->rc->config->get('ical_hidden_calendars', '')));
+
+ if (!empty($this->rc->user->ID)) {
+ $calendar_ids = array();
+ $result = $this->rc->db->query("
+ SELECT *, calendar_id AS id FROM " . $this->db_calendars . "
+ WHERE user_id=?
+ ORDER BY name",
+ $this->rc->user->ID
+ );
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $arr['showalarms'] = intval($arr['showalarms']);
+ $arr['active'] = !in_array($arr['id'], $hidden);
+ $arr['name'] = html::quote($arr['name']);
+ $arr['listname'] = html::quote($arr['name']);
+ $arr['rights'] = 'lrswikxteav';
+ $arr['editable'] = true;
+ $arr['ical_pass'] = $this->_decrypt_pass($arr['ical_pass']);
+ $this->calendars[$arr['calendar_id']] = $arr;
+ $calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
+
+ // Init sync client
+ $cal_id = $arr['calendar_id'];
+ $this->sync_clients[$cal_id] = new ical_sync($cal_id, $arr);
+ }
+ $this->calendar_ids = join(',', $calendar_ids);
+ }
+ }
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer Bitmask defining filter criterias
+ *
+ * @return array List of calendars
+ */
+ public function list_calendars($filter = 0)
+ {
+ $calendars = $this->calendars;
+
+ // filter active calendars
+ if ($filter & self::FILTER_ACTIVE) {
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ }
+
+ // 'personal' is unsupported in this driver
+
+ return array_map(function($cal) {
+
+ // Make calendar readonly
+ $cal["readonly"] = true;
+
+ // Readonly but deletable
+ $cal["deletable"] = true;
+
+ // But name should be editable!
+ $cal["editable_name"] = true;
+
+ return $cal;
+
+ }, $calendars);
+ }
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * @return mixed ID of the calendar on success, False on error
+ */
+ public function create_calendar($prop)
+ {
+ $result = $this->rc->db->query(
+ "INSERT INTO " . $this->db_calendars . "
+ (user_id, name, color, showalarms, ical_url, ical_user)
+ VALUES (?, ?, ?, ?, ?, ?)",
+ $this->rc->user->ID,
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms'] ? 1 : 0,
+ self::_encode_url($prop["ical_url"]),
+ isset($props["ical_user"]) ? $props["ical_user"] : null,
+ isset($props["ical_pass"]) ? $this->_encrypt_pass($props["ical_pass"]) : null
+ );
+
+ if ($result) {
+
+ $cal_id = $this->rc->db->insert_id($this->db_calendars);
+
+ // Initial sync of newly created calendars.
+ $this->sync_clients[$cal_id] = new ical_sync($cal_id, $prop);
+ $this->_sync_calendar($cal_id);
+
+ return $cal_id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function edit_calendar($prop)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_calendars . "
+ SET name=?, color=?, showalarms=?, ical_url=?, ical_user=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $prop['name'],
+ $prop['color'],
+ $prop['showalarms'] ? 1 : 0,
+ isset($prop["ical_url"]) ? $prop["ical_url"] : null,
+ isset($prop["ical_user"]) ? $prop["ical_user"] : null,
+ $prop['id'],
+ $this->rc->user->ID
+ );
+
+ // Change password if specified
+ if (isset($prop["ical_pass"])) {
+ $query = $this->rc->db->query("UPDATE " . $this->db_calendars . "
+ SET ical_pass=?
+ WHERE calendar_id=?
+ AND user_id=?",
+ $this->_encrypt_pass($prop['ical_pass']),
+ $prop['id'],
+ $this->rc->user->ID
+ );
+ }
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Set active/subscribed state of a calendar
+ * Save a list of hidden calendars in user prefs
+ *
+ * @see calendar_driver::subscribe_calendar()
+ */
+ public function subscribe_calendar($prop)
+ {
+ $hidden = array_flip(explode(',', $this->rc->config->get('ical_hidden_calendars', '')));
+
+ if ($prop['active'])
+ unset($hidden[$prop['id']]);
+ else
+ $hidden[$prop['id']] = 1;
+
+ return $this->rc->user->save_prefs(array('ical_hidden_calendars' => join(',', array_keys($hidden))));
+ }
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @see calendar_driver::delete_calendar()
+ */
+ public function delete_calendar($prop)
+ {
+ if (!$this->calendars[$prop['id']])
+ return false;
+
+ // events and attachments will be deleted by foreign key cascade
+
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_calendars . "
+ WHERE calendar_id=?",
+ $prop['id']
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Not supported by iCAL.
+ *
+ * @param $event
+ * @return bool
+ */
+ public function new_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Add a single event to the database
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::new_event()
+ */
+ private function _db_new_event($event)
+ {
+ if (!$this->validate($event))
+ return false;
+
+ if (!empty($this->calendars)) {
+ if ($event['calendar'] && !$this->calendars[$event['calendar']])
+ return false;
+ if (!$event['calendar'])
+ $event['calendar'] = reset(array_keys($this->calendars));
+
+ if ($event_id = $this->_insert_event($event)) {
+ $this->_update_recurring($event);
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ *
+ */
+ private function _insert_event(&$event)
+ {
+ $event = $this->_save_preprocess($event);
+
+ $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence,
+ title, description, location, categories, url, free_busy, priority, sensitivity, status, attendees, alarms, notifyat)
+ VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['calendar'],
+ strval($event['uid']),
+ intval($event['recurrence_id']),
+ strval($event['_instance']),
+ intval($event['isexception']),
+ $event['start']->format(self::DB_DATE_FORMAT),
+ $event['end']->format(self::DB_DATE_FORMAT),
+ intval($event['all_day']),
+ $event['_recurrence'],
+ strval($event['title']),
+ strval($event['description']),
+ strval($event['location']),
+ join(',', (array)$event['categories']),
+ strval($event['url']),
+ intval($event['free_busy']),
+ intval($event['priority']),
+ intval($event['sensitivity']),
+ strval($event['status']),
+ $event['attendees'],
+ $event['alarms'],
+ $event['notifyat']
+ );
+
+ $event_id = $this->rc->db->insert_id($this->db_events);
+
+ if ($event_id) {
+ $event['id'] = $event_id;
+
+ // add attachments
+ if (!empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event_id);
+ unset($attachment);
+ }
+ }
+
+ return $event_id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Not supported for iCAL
+ *
+ * @param $event
+ * @return bool
+ */
+ public function edit_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::edit_event()
+ */
+ private function _db_edit_event($event)
+ {
+ if (!empty($this->calendars)) {
+ $update_master = false;
+ $update_recurring = true;
+ $old = $this->get_event($event);
+ $ret = true;
+
+ // check if update affects scheduling and update attendee status accordingly
+ $reschedule = $this->_check_scheduling($event, $old, true);
+
+ // increment sequence number
+ if (empty($event['sequence']) && $reschedule)
+ $event['sequence'] = max($event['sequence'], $old['sequence']) + 1;
+
+ // modify a recurring event, check submitted savemode to do the right things
+ if ($old['recurrence'] || $old['recurrence_id']) {
+ $master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
+
+ // keep saved exceptions (not submitted by the client)
+ if ($old['recurrence']['EXDATE'])
+ $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
+
+ switch ($event['_savemode']) {
+ case 'new':
+ $event['uid'] = $this->cal->generate_uid();
+ return $this->new_event($event);
+
+ case 'current':
+ // save as exception
+ $event['isexception'] = 1;
+ $update_recurring = false;
+
+ // set exception to first instance (= master)
+ if ($event['id'] == $master['id']) {
+ $event += $old;
+ $event['recurrence_id'] = $master['id'];
+ $event['_instance'] = libcalendaring::recurrence_instance_identifier($old);
+ $event['isexception'] = 1;
+ $event_id = $this->_insert_event($event);
+ return $event_id;
+ }
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event, then save this instance as new recurring event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // if recurrence COUNT, update value to the correct number of future occurences
+ if ($event['recurrence']['COUNT']) {
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $sqlresult = $this->rc->db->query(sprintf(
+ "SELECT event_id FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND %s >= ?
+ AND recurrence_id=?",
+ $this->calendar_ids,
+ $this->rc->db->quote_identifier('start')
+ ),
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']);
+ if ($count = $this->rc->db->num_rows($sqlresult))
+ $event['recurrence']['COUNT'] = $count;
+ }
+
+ $update_recurring = true;
+ $event['recurrence_id'] = 0;
+ $event['isexception'] = 0;
+ $event['_instance'] = '';
+ break;
+ }
+ // else: 'future' == 'all' if modifying the master event
+
+ default: // 'all' is default
+ $event['id'] = $master['id'];
+ $event['recurrence_id'] = 0;
+
+ // use start date from master but try to be smart on time or duration changes
+ $old_start_date = $old['start']->format('Y-m-d');
+ $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
+ $old_duration = $old['end']->format('U') - $old['start']->format('U');
+
+ $new_start_date = $event['start']->format('Y-m-d');
+ $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
+ $new_duration = $event['end']->format('U') - $event['start']->format('U');
+
+ $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
+
+ // shifted or resized
+ if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
+ $event['start'] = $master['start']->add($old['start']->diff($event['start']));
+ $event['end'] = clone $event['start'];
+ $event['end']->add(new DateInterval('PT' . $new_duration . 'S'));
+ } // dates did not change, use the ones from master
+ else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
+ $event['start'] = $master['start'];
+ $event['end'] = $master['end'];
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if (is_array($event['recurrence']) && ($old_start_date != $new_start_date || $old_start_time != $new_start_time)
+ && ($exceptions = $this->_load_exceptions($old))
+ ) {
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ foreach ($exceptions as $exception) {
+ $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ if (is_a($recurrence_id, 'DateTime')) {
+ $recurrence_id->add($date_shift);
+ $exception['_instance'] = $recurrence_id->format($recurrence_id_format);
+ $this->_update_event($exception, false);
+ }
+ }
+ }
+
+ $ret = $event['id']; // return master ID
+ break;
+ }
+ }
+
+ $success = $this->_update_event($event, $update_recurring);
+
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
+ $update_event = $this->get_event(array('id' => $event['recurrence_id']));
+ $update_event['_savemode'] = $event['_savemode'];
+ calendar::merge_attendee_data($update_event, $attendees);
+ }
+
+ if ($ret = $this->update_attendees($update_event, $attendees)) {
+ // replace $event with effectively updated event (for iTip reply)
+ if ($ret !== true && $ret != $update_event['id'] && ($new_event = $this->get_event(array('id' => $ret)))) {
+ $event = $new_event;
+ } else {
+ $event = $update_event;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ $success = $this->edit_event($event, true);
+
+ // apply attendee updates to recurrence exceptions too
+ if ($success && $event['_savemode'] == 'all' && !empty($event['recurrence']) && empty($event['recurrence_id']) && ($exceptions = $this->_load_exceptions($event))) {
+ foreach ($exceptions as $exception) {
+ calendar::merge_attendee_data($exception, $attendees);
+ $this->_update_event($exception, false);
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Determine whether the current change affects scheduling and reset attendee status accordingly
+ */
+ private function _check_scheduling(&$event, $old, $update = true)
+ {
+ // skip this check when importing iCal/iTip events
+ if (isset($event['sequence']) || !empty($event['_method'])) {
+ return false;
+ }
+
+ $reschedule = false;
+
+ // iterate through the list of properties considered 'significant' for scheduling
+ foreach (self::$scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $event[$prop];
+ if ($event['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
+ unset($a['EXCEPTIONS'], $b['EXCEPTIONS']);
+ $a = array_filter($a);
+ $b = array_filter($b);
+
+ // advanced rrule comparison: no rescheduling if series was shortened
+ if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
+ unset($a['COUNT'], $b['COUNT']);
+ } else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
+ unset($a['UNTIL'], $b['UNTIL']);
+ }
+ }
+ if ($a != $b) {
+ $reschedule = true;
+ break;
+ }
+ }
+
+ // reset all attendee status to needs-action (#4360)
+ if ($update && $reschedule && is_array($event['attendees'])) {
+ $is_organizer = false;
+ $emails = $this->cal->get_user_emails();
+ $attendees = $event['attendees'];
+ foreach ($attendees as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $is_organizer = true;
+ } else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
+ $attendees[$i]['status'] = 'NEEDS-ACTION';
+ $attendees[$i]['rsvp'] = true;
+ }
+ }
+
+ // update attendees only if I'm the organizer
+ if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
+ $event['attendees'] = $attendees;
+ }
+ }
+
+ return $reschedule;
+ }
+
+ /**
+ * Convert save data to be used in SQL statements
+ */
+ private function _save_preprocess($event)
+ {
+ // shift dates to server's timezone (except for all-day events)
+ if (!$event['allday']) {
+ $event['start'] = clone $event['start'];
+ $event['start']->setTimezone($this->server_timezone);
+ $event['end'] = clone $event['end'];
+ $event['end']->setTimezone($this->server_timezone);
+ }
+
+ // compose vcalendar-style recurrencue rule from structured data
+ $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
+ $event['_recurrence'] = rtrim($rrule, ';');
+ $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
+ $event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]);
+
+ if ($event['free_busy'] == 'tentative') {
+ $event['status'] = 'TENTATIVE';
+ }
+
+ if (isset($event['allday'])) {
+ $event['all_day'] = $event['allday'] ? 1 : 0;
+ }
+
+ // compute absolute time to notify the user
+ $event['notifyat'] = $this->_get_notification($event);
+
+ if (is_array($event['valarms'])) {
+ $event['alarms'] = $this->serialize_alarms($event['valarms']);
+ }
+
+ // process event attendees
+ if (!empty($event['attendees']))
+ $event['attendees'] = json_encode((array)$event['attendees']);
+ else
+ $event['attendees'] = '';
+
+ return $event;
+ }
+
+ /**
+ * Compute absolute time to notify the user
+ */
+ private function _get_notification($event)
+ {
+ if ($event['valarms'] && $event['start'] > new DateTime()) {
+ $alarm = libcalendaring::get_next_alarm($event);
+
+ if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types))
+ return date('Y-m-d H:i:s', $alarm['time']);
+ }
+
+ return null;
+ }
+
+ /**
+ * Save the given event record to database
+ *
+ * @param array Event data
+ * @param boolean True if recurring events instances should be updated, too
+ */
+ private function _update_event($event, $update_recurring = true)
+ {
+ $event = $this->_save_preprocess($event);
+ $sql_set = array();
+ $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'isexception', 'sequence', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat');
+ foreach ($set_cols as $col) {
+ if (is_object($event[$col]) && is_a($event[$col], 'DateTime'))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT));
+ else if (is_array($event[$col]))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event[$col]));
+ else if (array_key_exists($col, $event))
+ $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
+ }
+
+ if ($event['_recurrence'])
+ $sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']);
+
+ if ($event['_instance'])
+ $sql_set[] = $this->rc->db->quote_identifier('instance') . '=' . $this->rc->db->quote($event['_instance']);
+
+ if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar'])
+ $sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']);
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s %s
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now(),
+ ($sql_set ? ', ' . join(', ', $sql_set) : '')
+ ),
+ $event['id']
+ );
+
+ $success = $this->rc->db->affected_rows($query);
+
+ // add attachments
+ if ($success && !empty($event['attachments'])) {
+ foreach ($event['attachments'] as $attachment) {
+ $this->add_attachment($attachment, $event['id']);
+ unset($attachment);
+ }
+ }
+
+ // remove attachments
+ if ($success && !empty($event['deleted_attachments'])) {
+ foreach ($event['deleted_attachments'] as $attachment) {
+ $this->remove_attachment($attachment, $event['id']);
+ }
+ }
+
+ if ($success) {
+ unset($this->cache[$event['id']]);
+ if ($update_recurring)
+ $this->_update_recurring($event);
+ }
+
+ return $success;
+ }
+
+ /**
+ * Insert "fake" entries for recurring occurences of this event
+ */
+ private function _update_recurring($event)
+ {
+ if (empty($this->calendars))
+ return;
+
+ if (!empty($event['recurrence'])) {
+ $exdata = array();
+ $exceptions = $this->_load_exceptions($event);
+
+ foreach ($exceptions as $exception) {
+ $exdate = substr($exception['_instance'], 0, 8);
+ $exdata[$exdate] = $exception;
+ }
+ }
+
+ // clear existing recurrence copies
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE recurrence_id=?
+ AND isexception=0
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $event['id']
+ );
+
+ // create new fake entries
+ if (!empty($event['recurrence'])) {
+ // include library class
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+
+ $recurrence = new calendar_recurrence($this->cal, $event);
+
+ $count = 0;
+ $event['allday'] = $event['all_day'];
+ $duration = $event['start']->diff($event['end']);
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ while ($next_start = $recurrence->next_start()) {
+ $instance = $next_start->format($recurrence_id_format);
+ $datestr = substr($instance, 0, 8);
+
+ // skip exceptions
+ // TODO: merge updated data from master event
+ if ($exdata[$datestr]) {
+ continue;
+ }
+
+ $next_start->setTimezone($this->server_timezone);
+ $next_end = clone $next_start;
+ $next_end->add($duration);
+
+ $notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end, 'status' => $event['status']));
+ $query = $this->rc->db->query(sprintf(
+ "INSERT INTO " . $this->db_events . "
+ (calendar_id, recurrence_id, created, changed, uid, instance, %s, %s, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, notifyat)
+ SELECT calendar_id, ?, %s, %s, uid, ?, ?, ?, all_day, sequence, recurrence, title, description, location, categories, url, free_busy, priority, sensitivity, status, alarms, attendees, ?
+ FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->quote_identifier('start'),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->now(),
+ $this->rc->db->now()
+ ),
+ $event['id'],
+ $instance,
+ $next_start->format(self::DB_DATE_FORMAT),
+ $next_end->format(self::DB_DATE_FORMAT),
+ $notify_at,
+ $event['id']
+ );
+
+ if (!$this->rc->db->affected_rows($query))
+ break;
+
+ // stop adding events for inifinite recurrence after 20 years
+ if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next_start->format('Y') > date('Y') + 20))
+ break;
+ }
+
+ // remove all exceptions after recurrence end
+ if ($next_end && !empty($exceptions)) {
+ $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `start` > ?
+ AND `calendar_id` IN (" . $this->calendar_ids . ")",
+ $event['id'],
+ $next_end->format(self::DB_DATE_FORMAT)
+ );
+ }
+ }
+ }
+
+ /**
+ *
+ */
+ private function _load_exceptions($event, $instance_id = null)
+ {
+ $sql_add_where = '';
+ if (!empty($instance_id)) {
+ $sql_add_where = 'AND `instance`=?';
+ }
+
+ $result = $this->rc->db->query(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE `recurrence_id`=?
+ AND `isexception`=1
+ AND `calendar_id` IN (" . $this->calendar_ids . ")
+ $sql_add_where
+ ORDER BY `instance`, `start`",
+ $event['id'],
+ $instance_id
+ );
+
+ $exceptions = array();
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $exception = $this->_read_postprocess($sql_arr);
+ $instance = $exception['_instance'] ?: $exception['start']->format($exception['allday'] ? 'Ymd' : 'Ymd\THis');
+ $exceptions[$instance] = $exception;
+ }
+
+ return $exceptions;
+ }
+
+ /**
+ * Move a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::move_event()
+ */
+ public function move_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Resize a single event
+ *
+ * @param array Hash array with event properties
+ * @see calendar_driver::resize_event()
+ */
+ public function resize_event($event)
+ {
+ // let edit_event() do all the magic
+ return $this->edit_event($event + (array)$this->get_event($event));
+ }
+
+ /**
+ * Not supported by iCAL
+ *
+ * @param $event
+ * @param bool $force
+ * @return bool
+ */
+ public function remove_event($event, $force = true)
+ {
+ return false;
+ }
+
+ /**
+ * Remove a single event from the database
+ *
+ * @param array Hash array with event properties
+ * @param boolean Remove record irreversible (@TODO)
+ *
+ * @see calendar_driver::remove_event()
+ */
+ private function _db_remove_event($event, $force = true)
+ {
+ if (!empty($this->calendars)) {
+ $event += (array)$this->get_event($event);
+ $master = $event;
+ $update_master = false;
+ $savemode = 'all';
+ $ret = true;
+
+ // read master if deleting a recurring event
+ if ($event['recurrence'] || $event['recurrence_id']) {
+ $master = $event['recurrence_id'] ? $this->get_event(array('id' => $event['recurrence_id'])) : $event;
+ $savemode = $event['_savemode'];
+ }
+
+ switch ($savemode) {
+ case 'current':
+ // add exception to master event
+ $master['recurrence']['EXDATE'][] = $event['start'];
+ $update_master = true;
+
+ // just delete this single occurence
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND event_id=?",
+ $event['id']
+ );
+ break;
+
+ case 'future':
+ if ($master['id'] != $event['id']) {
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+ $update_master = true;
+
+ // delete this and all future instances
+ $fromdate = clone $event['start'];
+ $fromdate->setTimezone($this->server_timezone);
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE calendar_id IN (" . $this->calendar_ids . ")
+ AND " . $this->rc->db->quote_identifier('start') . " >= ?
+ AND recurrence_id=?",
+ $fromdate->format(self::DB_DATE_FORMAT),
+ $master['id']
+ );
+ $ret = $master['id'];
+ break;
+ }
+ // else: future == all if modifying the master event
+
+ default: // 'all' is default
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_events . "
+ WHERE (event_id=? OR recurrence_id=?)
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $master['id'],
+ $master['id']
+ );
+ break;
+ }
+
+ $success = $this->rc->db->affected_rows($query);
+ if ($success && $update_master)
+ $this->_update_event($master, true);
+
+ return $success ? $ret : false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return data of a specific event
+ * @param mixed Hash array with event properties or event UID
+ * @param integer Bitmask defining the scope to search events in
+ * @param boolean If true, recurrence exceptions shall be added
+ * @return array Hash array with event properties
+ */
+ public function get_event($event, $scope = 0, $full = false)
+ {
+ $id = is_array($event) ? ($event['id'] ?: $event['uid']) : $event;
+ $cal = is_array($event) ? $event['calendar'] : null;
+ $col = is_array($event) && is_numeric($id) ? 'event_id' : 'uid';
+
+ $where_add = '';
+ if (is_array($event) && !$event['id'] && !empty($event['_instance'])) {
+ $where_add = 'AND instance=' . $this->rc->db->quote($event['_instance']);
+ }
+
+ if ($this->cache[$id])
+ return $this->cache[$id];
+
+ if ($scope & self::FILTER_ACTIVE) {
+ $calendars = $this->calendars;
+ foreach ($calendars as $idx => $cal) {
+ if (!$cal['active']) {
+ unset($calendars[$idx]);
+ }
+ }
+ $cals = join(',', $calendars);
+ } else {
+ $cals = $this->calendar_ids;
+ }
+
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " AS e
+ WHERE e.calendar_id IN (%s)
+ AND e.$col=?
+ %s",
+ $cals,
+ $where_add
+ ),
+ $id);
+
+ if ($result && ($sql_arr = $this->rc->db->fetch_assoc($result)) && $sql_arr['event_id']) {
+ $event = $this->_read_postprocess($sql_arr);
+
+ // also load recurrence exceptions
+ if (!empty($event['recurrence']) && $full) {
+ $event['recurrence']['EXCEPTIONS'] = array_values($this->_load_exceptions($event));
+ }
+
+ $this->cache[$id] = $event;
+ return $this->cache[$id];
+ }
+
+ return false;
+ }
+
+ /**
+ * Sync and returns event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_intersect($calendars, array_keys($this->calendars));
+
+ // Make sure that the calendars are in sync.
+ foreach ($calendar_ids as $cal_id) {
+ if (!$this->_is_synced($cal_id))
+ $this->_sync_calendar($cal_id);
+ }
+
+ return $this->_db_load_events($start, $end, $query, $calendars, $virtual, $modifiedsince);
+ }
+
+ /**
+ * Get event data
+ *
+ * @see calendar_driver::load_events()
+ */
+ private function _db_load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (!is_array($calendars))
+ $calendars = explode(',', strval($calendars));
+
+ // only allow to select from calendars of this use
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars)));
+
+ // compose (slow) SQL query for searching
+ // FIXME: improve searching using a dedicated col and normalized values
+ if ($query) {
+ foreach (array('title', 'location', 'description', 'categories', 'attendees') as $col)
+ $sql_query[] = $this->rc->db->ilike($col, '%' . $query . '%');
+ $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
+ }
+
+ if (!$virtual)
+ $sql_add .= ' AND e.recurrence_id = 0';
+
+ if ($modifiedsince)
+ $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince));
+
+ $events = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT e.*, (SELECT COUNT(attachment_id) FROM " . $this->db_attachments . "
+ WHERE event_id = e.event_id OR event_id = e.recurrence_id) AS _attachments
+ FROM " . $this->db_events . " e
+ WHERE e.calendar_id IN (%s)
+ AND e.start <= %s AND e.end >= %s
+ %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($end),
+ $this->rc->db->fromunixtime($start),
+ $sql_add
+ ));
+
+ while ($result && ($sql_arr = $this->rc->db->fetch_assoc($result))) {
+ $event = $this->_read_postprocess($sql_arr);
+ $add = true;
+
+ if (!empty($event['recurrence']) && !$event['recurrence_id']) {
+ // load recurrence exceptions (i.e. for export)
+ if (!$virtual) {
+ $event['recurrence']['EXCEPTIONS'] = $this->_load_exceptions($event);
+ } // check for exception on first instance
+ else {
+ $instance = libcalendaring::recurrence_instance_identifier($event);
+ $exceptions = $this->_load_exceptions($event, $instance);
+ if ($exceptions && is_array($exceptions[$instance])) {
+ $event = $exceptions[$instance];
+ $add = false;
+ }
+ }
+ }
+
+ if ($add)
+ $events[] = $event;
+ }
+ }
+
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ public function count_events($calendars, $start, $end = null)
+ {
+ // not implemented
+ return array();
+ }
+
+ /**
+ * Convert sql record into a rcube style event object
+ */
+ private function _read_postprocess($event)
+ {
+ $free_busy_map = array_flip($this->free_busy_map);
+ $sensitivity_map = array_flip($this->sensitivity_map);
+
+ $event['id'] = $event['event_id'];
+ $event['start'] = new DateTime($event['start']);
+ $event['end'] = new DateTime($event['end']);
+ $event['allday'] = intval($event['all_day']);
+ $event['created'] = new DateTime($event['created']);
+ $event['changed'] = new DateTime($event['changed']);
+ $event['free_busy'] = $free_busy_map[$event['free_busy']];
+ $event['sensitivity'] = $sensitivity_map[$event['sensitivity']];
+ $event['calendar'] = $event['calendar_id'];
+ $event['recurrence_id'] = intval($event['recurrence_id']);
+ $event['isexception'] = intval($event['isexception']);
+
+ // parse recurrence rule
+ if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
+ $event['recurrence'] = array();
+ foreach ($m as $rr) {
+ if (is_numeric($rr[2]))
+ $rr[2] = intval($rr[2]);
+ else if ($rr[1] == 'UNTIL')
+ $rr[2] = date_create($rr[2]);
+ else if ($rr[1] == 'RDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ else if ($rr[1] == 'EXDATE')
+ $rr[2] = array_map('date_create', explode(',', $rr[2]));
+ $event['recurrence'][$rr[1]] = $rr[2];
+ }
+ }
+
+ if ($event['recurrence_id']) {
+ libcalendaring::identify_recurrence_instance($event);
+ }
+
+ if (strlen($event['instance'])) {
+ $event['_instance'] = $event['instance'];
+
+ if (empty($event['recurrence_id'])) {
+ $event['recurrence_date'] = rcube_utils::anytodatetime($event['_instance'], $event['start']->getTimezone());
+ }
+ }
+
+ if ($event['_attachments'] > 0) {
+ $event['attachments'] = (array)$this->list_attachments($event);
+ }
+
+ // decode serialized event attendees
+ if (strlen($event['attendees'])) {
+ $event['attendees'] = $this->unserialize_attendees($event['attendees']);
+ } else {
+ $event['attendees'] = array();
+ }
+
+ // decode serialized alarms
+ if ($event['alarms']) {
+ $event['valarms'] = $this->unserialize_alarms($event['alarms']);
+ }
+
+ unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['instance'], $event['_attachments']);
+ return $event;
+ }
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @see calendar_driver::pending_alarms()
+ */
+ public function pending_alarms($time, $calendars = null)
+ {
+ if (empty($calendars))
+ $calendars = array_keys($this->calendars);
+ else if (is_string($calendars))
+ $calendars = explode(',', $calendars);
+
+ // only allow to select from calendars with activated alarms
+ $calendar_ids = array();
+ foreach ($calendars as $cid) {
+ if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms'])
+ $calendar_ids[] = $cid;
+ }
+ $calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids);
+
+ $alarms = array();
+ if (!empty($calendar_ids)) {
+ $result = $this->rc->db->query(sprintf(
+ "SELECT * FROM " . $this->db_events . "
+ WHERE calendar_id IN (%s)
+ AND notifyat <= %s AND %s > %s",
+ join(',', $calendar_ids),
+ $this->rc->db->fromunixtime($time),
+ $this->rc->db->quote_identifier('end'),
+ $this->rc->db->fromunixtime($time)
+ ));
+
+ while ($result && ($event = $this->rc->db->fetch_assoc($result)))
+ $alarms[] = $this->_read_postprocess($event);
+ }
+
+ return $alarms;
+ }
+
+ /**
+ * Feedback after showing/sending an alarm notification
+ *
+ * @see calendar_driver::dismiss_alarm()
+ */
+ public function dismiss_alarm($event_id, $snooze = 0)
+ {
+ // set new notifyat time or unset if not snoozed
+ $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null;
+
+ $query = $this->rc->db->query(sprintf(
+ "UPDATE " . $this->db_events . "
+ SET changed=%s, notifyat=?
+ WHERE event_id=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $this->rc->db->now()),
+ $notify_at,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Save an attachment related to the given event
+ */
+ private function add_attachment($attachment, $event_id)
+ {
+ $data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
+
+ $query = $this->rc->db->query(
+ "INSERT INTO " . $this->db_attachments .
+ " (event_id, filename, mimetype, size, data)" .
+ " VALUES (?, ?, ?, ?, ?)",
+ $event_id,
+ $attachment['name'],
+ $attachment['mimetype'],
+ strlen($data),
+ base64_encode($data)
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Remove a specific attachment from the given event
+ */
+ private function remove_attachment($attachment_id, $event_id)
+ {
+ $query = $this->rc->db->query(
+ "DELETE FROM " . $this->db_attachments .
+ " WHERE attachment_id = ?" .
+ " AND event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id = ?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))",
+ $attachment_id,
+ $event_id
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * List attachments of specified event
+ */
+ public function list_attachments($event)
+ {
+ $attachments = array();
+
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE event_id IN (SELECT event_id FROM " . $this->db_events .
+ " WHERE event_id=?" .
+ " AND calendar_id IN (" . $this->calendar_ids . "))" .
+ " ORDER BY filename",
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id']
+ );
+
+ while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $attachments[] = $arr;
+ }
+ }
+
+ return $attachments;
+ }
+
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT attachment_id AS id, filename AS name, mimetype, size " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?" .
+ " AND event_id=?",
+ $id,
+ $event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return $arr;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get attachment body
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!empty($this->calendar_ids)) {
+ $result = $this->rc->db->query(
+ "SELECT data " .
+ " FROM " . $this->db_attachments .
+ " WHERE attachment_id=?" .
+ " AND event_id=?",
+ $id,
+ $event['id']
+ );
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ return base64_decode($arr['data']);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove the given category
+ */
+ public function remove_category($name)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=''
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Update/replace a category
+ */
+ public function replace_category($oldname, $name, $color)
+ {
+ $query = $this->rc->db->query(
+ "UPDATE " . $this->db_events . "
+ SET categories=?
+ WHERE categories=?
+ AND calendar_id IN (" . $this->calendar_ids . ")",
+ $name,
+ $oldname
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * Helper method to serialize the list of alarms into a string
+ */
+ private function serialize_alarms($valarms)
+ {
+ foreach ((array)$valarms as $i => $alarm) {
+ if ($alarm['trigger'] instanceof DateTime) {
+ $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c');
+ }
+ }
+
+ return $valarms ? json_encode($valarms) : null;
+ }
+
+ /**
+ * Helper method to decode a serialized list of alarms
+ */
+ private function unserialize_alarms($alarms)
+ {
+ // decode json serialized alarms
+ if ($alarms && $alarms[0] == '[') {
+ $valarms = json_decode($alarms, true);
+ foreach ($valarms as $i => $alarm) {
+ if ($alarm['trigger'][0] == '@') {
+ try {
+ $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1));
+ } catch (Exception $e) {
+ unset($valarms[$i]);
+ }
+ }
+ }
+ } // convert legacy alarms data
+ else if (strlen($alarms)) {
+ list($trigger, $action) = explode(':', $alarms, 2);
+ if ($trigger = libcalendaring::parse_alaram_value($trigger)) {
+ $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0]));
+ }
+ }
+
+ return $valarms;
+ }
+
+ /**
+ * Helper method to decode the attendees list from string
+ */
+ private function unserialize_attendees($s_attendees)
+ {
+ $attendees = array();
+
+ // decode json serialized string
+ if ($s_attendees[0] == '[') {
+ $attendees = json_decode($s_attendees, true);
+ } // decode the old serialization format
+ else {
+ foreach (explode("\n", $event['attendees']) as $line) {
+ $att = array();
+ foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) {
+ list($key, $value) = explode("=", $prop);
+ $att[strtolower($key)] = stripslashes(trim($value, '""'));
+ }
+ $attendees[] = $att;
+ }
+ }
+
+ return $attendees;
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ $db = $this->rc->db;
+ $user = $args['user'];
+ $event_ids = array();
+
+ $events = $db->query(
+ "SELECT event_id FROM " . $this->db_events . " AS ev" .
+ " LEFT JOIN " . $this->db_calendars . " cal ON (ev.calendar_id = cal.calendar_id)" .
+ " WHERE user_id=?",
+ $user->ID);
+
+ while ($row = $db->fetch_assoc($events)) {
+ $event_ids[] = $row['event_id'];
+ }
+
+ if (!empty($event_ids)) {
+ foreach (array($this->db_attachments, $this->db_events) as $table) {
+ $db->query(sprintf("DELETE FROM $table WHERE event_id IN (%s)", join(',', $event_ids)));
+ }
+ }
+
+ foreach (array($this->db_calendars, 'itipinvitations') as $table) {
+ $db->query("DELETE FROM $table WHERE user_id=?", $user->ID);
+ }
+ }
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string Request action 'form-edit|form-new'
+ * @param array Calendar properties (e.g. id, color)
+ * @param array Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ // Make sure we have current attributes
+ $calendar = $this->calendars[$calendar["id"]];
+
+ $input_ical_url = new html_inputfield(array(
+ "name" => "ical_url",
+ "id" => "ical_url",
+ "size" => 20
+ ));
+
+ $formfields["ical_url"] = array(
+ "label" => $this->cal->gettext("icalurl"),
+ "value" => $input_ical_url->show($calendar["ical_url"]),
+ "id" => "ical_url",
+ );
+
+ $input_ical_user = new html_inputfield( array(
+ "name" => "ical_user",
+ "id" => "ical_user",
+ "size" => 20
+ ));
+
+ $formfields["ical_user"] = array(
+ "label" => $this->cal->gettext("username"),
+ "value" => $input_ical_user->show($calendar["ical_user"]),
+ "id" => "ical_user",
+ );
+
+ $input_ical_pass = new html_passwordfield( array(
+ "name" => "ical_pass",
+ "id" => "ical_pass",
+ "size" => 20
+ ));
+
+ $formfields["ical_pass"] = array(
+ "label" => $this->cal->gettext("password"),
+ "value" => $input_ical_pass->show(null), // Don't send plain text password to GUI
+ "id" => "ical_pass",
+ );
+
+ return parent::calendar_form($action, $calendar, $formfields);
+ }
+
+ /**
+ * Determines whether the given calendar is in sync regarding the configured sync period.
+ *
+ * @param int Calender id.
+ * @return boolean True if calendar is in sync, true otherwise.
+ */
+ private function _is_synced($cal_id)
+ {
+ // Atomic sql: Check for exceeded sync period and update last_change.
+ $query = $this->rc->db->query(
+ "UPDATE ".$this->db_calendars." ".
+ "SET ical_last_change = NOW() WHERE calendar_id = ? AND ".
+ $this->_unix_timestamp('ical_last_change') ." + ? <= ".$this->_unix_timestamp('NOW()'),
+ $cal_id, $this->sync_period);
+
+ if($query->rowCount() > 0)
+ {
+ $is_synced = $this->sync_clients[$cal_id]->is_synced();
+ self::debug_log("Calendar \"$cal_id\" ".($is_synced ? "is in sync" : "needs update").".");
+ return $is_synced;
+ }
+ else
+ {
+ self::debug_log("Sync period active: Assuming calendar \"$cal_id\" to be in sync.");
+ return true;
+ }
+ }
+
+ /**
+ * Returns db-specific timestamp queries for epoch format
+ *
+ * @param str column name or valid timestamp (e.g. NOW())
+ * @return str db-specific timestamp query for epoch format
+ */
+ private function _unix_timestamp($field)
+ {
+ switch ($this->rc->db->db_provider) {
+ case 'postgres':
+ return "EXTRACT (EPOCH FROM $field)";
+ default:
+ return "UNIX_TIMESTAMP($field)";
+ }
+ }
+
+ /**
+ * Encodes directory- and filenames using rawurlencode().
+ *
+ * @see http://stackoverflow.com/questions/7973790/urlencode-only-the-directory-and-file-names-of-a-url
+ * @param string Unencoded URL to be encoded.
+ * @return Encoded URL.
+ */
+ private static function _encode_url($url)
+ {
+ // Don't encode if "%" is already used.
+ if (strstr($url, "%") === false) {
+ return preg_replace_callback('#://([^/]+)/([^?]+)#', function ($matches) {
+ return '://' . $matches[1] . '/' . join('/', array_map('rawurlencode', explode('/', $matches[2])));
+ }, $url);
+ } else return $url;
+ }
+
+ /**
+ * Performs iCAL updates on given events.
+ *
+ * @param array ical and event properties to update. See ical_sync::get_updates().
+ * @return array List of event ids.
+ */
+ private function _perform_updates($updates)
+ {
+ $event_ids = array();
+
+ $num_created = 0;
+ $num_updated = 0;
+
+ foreach ($updates as $update) {
+ // local event -> update event
+ if (isset($update["local_event"])) {
+ // let edit_event() do all the magic
+ if ($this->_db_edit_event($update["remote_event"] + (array)$update["local_event"])) {
+
+ $event_id = $update["local_event"]["id"];
+ array_push($event_ids, $event_id);
+
+ $num_updated++;
+
+ self::debug_log("Updated event \"$event_id\".");
+
+ } else {
+ self::debug_log("Could not perform event update: " . print_r($update, true));
+ }
+ } // no local event -> create event
+ else {
+ $event_id = $this->_db_new_event($update["remote_event"]);
+ if ($event_id) {
+
+ array_push($event_ids, $event_id);
+
+ $num_created++;
+
+ self::debug_log("Created event \"$event_id\".");
+
+ } else {
+ self::debug_log("Could not perform event creation: " . print_r($update, true));
+ }
+ }
+ }
+
+ self::debug_log("Created $num_created new events, updated $num_updated event.");
+ return $event_ids;
+ }
+
+ /**
+ * Return all events from the given calendar.
+ *
+ * @param int Calendar id.
+ * @return array
+ */
+ private function _load_all_events($cal_id)
+ {
+ // FIXME: This is kind of ugly but a way to get _all_ events without touching the database driver.
+
+ // Get the event with the maximum end time.
+ $result = $this->rc->db->query(
+ "SELECT MAX(e.end) as end FROM " . $this->db_events . " e " .
+ "WHERE e.calendar_id = ? ", $cal_id);
+
+ if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+ $end = new DateTime($arr["end"]);
+
+ // Don't use load_events() which is doing another sync while this method might be already invoked in an sync.
+ return $this->_db_load_events(0, $end->getTimestamp(), null, array($cal_id));
+ }
+ else return array();
+ }
+
+ /**
+ * Synchronizes events of given calendar.
+ *
+ * @param int Calendar id.
+ */
+ private function _sync_calendar($cal_id)
+ {
+ self::debug_log("Syncing calendar id \"$cal_id\".");
+
+ $cal_sync = $this->sync_clients[$cal_id];
+ $events = array();
+
+ // Ignore recurrence events
+ foreach ($this->_load_all_events($cal_id) as $event) {
+ if ($event["recurrence_id"] == 0) {
+ array_push($events, $event);
+ }
+ }
+
+ $updates = $cal_sync->get_updates($events);
+ if($updates)
+ {
+ list($updates, $synced_event_ids) = $updates;
+ $updated_event_ids = $this->_perform_updates($updates);
+
+ // Delete events that are not in sync or updated.
+ foreach ($events as $event) {
+ if (array_search($event["id"], $updated_event_ids) === false &&
+ array_search($event["id"], $synced_event_ids) === false)
+ {
+ // Assume: Event was not updated, so delete!
+ $this->_db_remove_event($event, true);
+ self::debug_log("Remove event \"" . $event["id"] . "\".");
+ }
+ }
+ }
+
+ self::debug_log("Successfully synced calendar id \"$cal_id\".");
+ }
+
+ private function _decrypt_pass($pass) {
+ $p = base64_decode($pass);
+ $e = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);
+ return $e->decrypt($p, $this->crypt_key);
+ }
+
+ private function _encrypt_pass($pass) {
+ $e = new Encryption(MCRYPT_BlOWFISH, MCRYPT_MODE_CBC);
+ $p = $e->encrypt($pass, $this->crypt_key);
+ return base64_encode($p);
+ }
+}
diff --git a/calendar/drivers/ical/ical_sync.php b/calendar/drivers/ical/ical_sync.php
new file mode 100644
index 0000000..2b50af1
--- /dev/null
+++ b/calendar/drivers/ical/ical_sync.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * iCalendar sync for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class ical_sync
+{
+ const ACTION_NONE = 1;
+ const ACTION_UPDATE = 2;
+ const ACTION_CREATE = 4;
+
+ private $cal_id = null;
+ private $url = null;
+ private $user = null;
+ private $pass = null;
+ private $ical = null;
+
+ /**
+ * Default constructor for calendar synchronization adapter.
+ *
+ * @param int Calendar id.
+ * @param array Hash array with ical properties:
+ * url: Absolute URL to iCAL resource.
+ */
+ public function __construct($cal_id, $props)
+ {
+ $this->ical = libcalendaring::get_ical();
+ $this->cal_id = $cal_id;
+
+ $this->url = $props["ical_url"];
+ $this->user = isset($props["ical_user"]) ? $props["ical_user"] : null;
+ $this->pass = isset($props["ical_pass"]) ? $props["ical_pass"] : null;
+ }
+
+ /**
+ * Determines whether current calendar needs to be synced.
+ *
+ * @return True if the current calendar needs to be synced, false otherwise.
+ */
+ public function is_synced()
+ {
+ // No change to check that so far.
+ return false;
+ }
+
+ /**
+ * Fetches events from iCAL resource and returns updates.
+ *
+ * @param array List of local events.
+ * @return array Tuple containing the following lists:
+ *
+ * Hash list for iCAL events to be created or to be updated with the keys:
+ * local_event: The local event in case of an update.
+ * remote_event: The current event retrieved from caldav server.
+ *
+ * A list of event ids that are in sync.
+ */
+ public function get_updates($events)
+ {
+ $context = null;
+ if($this->user != null && $this->pass != null)
+ {
+ $context = stream_context_create(array(
+ 'http' => array(
+ 'header' => "Authorization: Basic " . base64_encode("$this->user:$this->pass")
+ )
+ ));
+ }
+
+ $vcal = file_get_contents($this->url, false, $context);
+ $updates = array();
+ $synced = array();
+ if($vcal !== false) {
+
+ // Hash existing events by uid.
+ $events_hash = array();
+ foreach($events as $event) {
+ $events_hash[$event['uid']] = $event;
+ }
+
+ foreach ($this->ical->import($vcal) as $remote_event) {
+
+ // Attach remote event to current calendar
+ $remote_event["calendar"] = $this->cal_id;
+
+ $local_event = null;
+ if($events_hash[$remote_event['uid']])
+ $local_event = $events_hash[$remote_event['uid']];
+
+ // Determine whether event don't need an update.
+ if($local_event && $local_event["changed"] >= $remote_event["changed"])
+ {
+ array_push($synced, $local_event["id"]);
+ }
+ else
+ {
+ array_push($updates, array('local_event' => $local_event, 'remote_event' => $remote_event));
+ }
+ }
+ }
+
+ return array($updates, $synced);
+ }
+}
+
+;
+?> \ No newline at end of file
diff --git a/calendar/drivers/kolab/SQL/mysql.initial.sql b/calendar/drivers/kolab/SQL/mysql.initial.sql
new file mode 100644
index 0000000..d500961
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/mysql.initial.sql
@@ -0,0 +1,32 @@
+/**
+ * Roundcube Calendar Kolab backend
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ **/
+
+CREATE TABLE IF NOT EXISTS `kolab_alarms` (
+ `alarm_id` VARCHAR(255) NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL,
+ `notifyat` DATETIME DEFAULT NULL,
+ `dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`alarm_id`,`user_id`),
+ CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */;
+
+CREATE TABLE IF NOT EXISTS `itipinvitations` (
+ `token` VARCHAR(64) NOT NULL,
+ `event_uid` VARCHAR(255) NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
+ `event` TEXT NOT NULL,
+ `expires` DATETIME DEFAULT NULL,
+ `cancelled` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`token`),
+ INDEX `uid_idx` (`event_uid`,`user_id`),
+ CONSTRAINT `fk_itipinvitations_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+REPLACE INTO system (name, value) VALUES ('calendar-kolab-version', '2014041700');
diff --git a/calendar/drivers/kolab/SQL/mysql/2012080600.sql b/calendar/drivers/kolab/SQL/mysql/2012080600.sql
new file mode 100644
index 0000000..5c9f1ae
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/mysql/2012080600.sql
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS `kolab_alarms`;
+
+CREATE TABLE `kolab_alarms` (
+ `event_id` VARCHAR(255) NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL,
+ `notifyat` DATETIME DEFAULT NULL,
+ `dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
+ PRIMARY KEY(`event_id`),
+ CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=INNODB */;
diff --git a/calendar/drivers/kolab/SQL/mysql/2013011000.sql b/calendar/drivers/kolab/SQL/mysql/2013011000.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/mysql/2013011000.sql
@@ -0,0 +1 @@
+-- empty \ No newline at end of file
diff --git a/calendar/drivers/kolab/SQL/mysql/2014041700.sql b/calendar/drivers/kolab/SQL/mysql/2014041700.sql
new file mode 100644
index 0000000..9175b55
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/mysql/2014041700.sql
@@ -0,0 +1 @@
+ALTER TABLE `kolab_alarms` CHANGE `event_id` `alarm_id` VARCHAR(255) NOT NULL; \ No newline at end of file
diff --git a/calendar/drivers/kolab/SQL/mysql/2014082600.sql b/calendar/drivers/kolab/SQL/mysql/2014082600.sql
new file mode 100644
index 0000000..501eb5c
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/mysql/2014082600.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `kolab_alarms` DROP PRIMARY KEY;
+ALTER TABLE `kolab_alarms` ADD PRIMARY KEY (`alarm_id`, `user_id`);
diff --git a/calendar/drivers/kolab/SQL/oracle.initial.sql b/calendar/drivers/kolab/SQL/oracle.initial.sql
new file mode 100644
index 0000000..d6d882b
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/oracle.initial.sql
@@ -0,0 +1,31 @@
+/**
+ * Roundcube Calendar Kolab backend
+ *
+ * @author Aleksander Machniak
+ * @licence GNU AGPL
+ **/
+
+CREATE TABLE "kolab_alarms" (
+ "alarm_id" varchar(255) NOT NULL PRIMARY KEY,
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "notifyat" timestamp DEFAULT NULL,
+ "dismissed" smallint DEFAULT 0 NOT NULL
+);
+
+CREATE INDEX "kolab_alarms_user_id_idx" ON "kolab_alarms" ("user_id");
+
+
+CREATE TABLE "itipinvitations" (
+ "token" varchar(64) NOT NULL PRIMARY KEY,
+ "event_uid" varchar(255) NOT NULL,
+ "user_id" integer NOT NULL
+ REFERENCES "users" ("user_id") ON DELETE CASCADE,
+ "event" long NOT NULL,
+ "expires" timestamp DEFAULT NULL,
+ "cancelled" smallint DEFAULT 0 NOT NULL
+);
+
+CREATE INDEX "itipinvitations_user_id_idx" ON "itipinvitations" ("user_id", "event_uid");
+
+INSERT INTO "system" ("name", "value") VALUES ('calendar-kolab-version', '2014041700');
diff --git a/calendar/drivers/kolab/SQL/postgres.initial.sql b/calendar/drivers/kolab/SQL/postgres.initial.sql
new file mode 100644
index 0000000..11dff41
--- /dev/null
+++ b/calendar/drivers/kolab/SQL/postgres.initial.sql
@@ -0,0 +1,32 @@
+/**
+ * Roundcube Calendar Kolab backend
+ *
+ * @author Sergey Sidlyarenko
+ * @licence GNU AGPL
+ **/
+
+CREATE TABLE IF NOT EXISTS kolab_alarms (
+ alarm_id character varying(255) NOT NULL,
+ user_id integer NOT NULL
+ REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+ notifyat timestamp without time zone DEFAULT NULL,
+ dismissed smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY(alarm_id)
+);
+
+CREATE INDEX kolab_alarms_user_id_idx ON kolab_alarms (user_id);
+
+CREATE TABLE IF NOT EXISTS itipinvitations (
+ token character varying(64) NOT NULL,
+ event_uid character varying(255) NOT NULL,
+ user_id integer NOT NULL
+ REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE,
+ event text NOT NULL,
+ expires timestamp without time zone DEFAULT NULL,
+ cancelled smallint NOT NULL DEFAULT 0,
+ PRIMARY KEY(token)
+);
+
+CREATE INDEX itipinvitations_user_id_event_uid_idx ON itipinvitations (user_id, event_uid);
+
+INSERT INTO system (name, value) VALUES ('calendar-kolab-version', '2014041700');
diff --git a/calendar/drivers/kolab/kolab_calendar.php b/calendar/drivers/kolab/kolab_calendar.php
new file mode 100644
index 0000000..19a03e1
--- /dev/null
+++ b/calendar/drivers/kolab/kolab_calendar.php
@@ -0,0 +1,836 @@
+<?php
+
+/**
+ * Kolab calendar storage class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class kolab_calendar extends kolab_storage_folder_api
+{
+ public $ready = false;
+ public $rights = 'lrs';
+ public $editable = false;
+ public $attachments = true;
+ public $alarms = false;
+ public $history = false;
+ public $subscriptions = true;
+ public $categories = array();
+ public $storage;
+
+ public $type = 'event';
+
+ protected $cal;
+ protected $events = array();
+ protected $search_fields = array('title', 'description', 'location', 'attendees');
+
+ /**
+ * Factory method to instantiate a kolab_calendar object
+ *
+ * @param string Calendar ID (encoded IMAP folder name)
+ * @param object calendar plugin object
+ * @return object kolab_calendar instance
+ */
+ public static function factory($id, $calendar)
+ {
+ $imap = $calendar->rc->get_storage();
+ $imap_folder = kolab_storage::id_decode($id);
+ $info = $imap->folder_info($imap_folder, true);
+ if (empty($info) || $info['noselect'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
+ return new kolab_user_calendar($imap_folder, $calendar);
+ }
+ else {
+ return new kolab_calendar($imap_folder, $calendar);
+ }
+ }
+
+ /**
+ * Default constructor
+ */
+ public function __construct($imap_folder, $calendar)
+ {
+ $this->cal = $calendar;
+ $this->imap = $calendar->rc->get_storage();
+ $this->name = $imap_folder;
+
+ // ID is derrived from folder name
+ $this->id = kolab_storage::folder_id($this->name, true);
+ $old_id = kolab_storage::folder_id($this->name, false);
+
+ // fetch objects from the given IMAP folder
+ $this->storage = kolab_storage::get_folder($this->name);
+ $this->ready = $this->storage && $this->storage->valid;
+
+ // Set writeable and alarms flags according to folder permissions
+ if ($this->ready) {
+ if ($this->storage->get_namespace() == 'personal') {
+ $this->editable = true;
+ $this->rights = 'lrswikxteav';
+ $this->alarms = true;
+ }
+ else {
+ $rights = $this->storage->get_myrights();
+ if ($rights && !PEAR::isError($rights)) {
+ $this->rights = $rights;
+ if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
+ $this->editable = strpos($rights, 'i');;
+ }
+ }
+
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+ if (isset($prefs[$this->id]['showalarms']))
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ else if (isset($prefs[$old_id]['showalarms']))
+ $this->alarms = $prefs[$old_id]['showalarms'];
+ }
+
+ $this->default = $this->storage->default;
+ $this->subtype = $this->storage->subtype;
+ }
+
+
+ /**
+ * Getter for the IMAP folder name
+ *
+ * @return string Name of the IMAP folder
+ */
+ public function get_realname()
+ {
+ return $this->name;
+ }
+
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return null;
+ }
+
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color()
+ {
+ // color is defined in folder METADATA
+ if ($color = $this->storage->get_color()) {
+ return $color;
+ }
+
+ // calendar color is stored in user prefs (temporary solution)
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+ return $prefs[$this->id]['color'];
+
+ return 'cc0000';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
+ return strtr($template, array(
+ '%h' => $_SERVER['HTTP_HOST'],
+ '%u' => urlencode($this->cal->rc->get_user_name()),
+ '%i' => urlencode($this->storage->get_uid()),
+ '%n' => urlencode($this->name),
+ ));
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ $prop['oldname'] = $this->get_realname();
+ $newfolder = kolab_storage::folder_update($prop);
+
+ if ($newfolder === false) {
+ $this->cal->last_error = $this->cal->gettext(kolab_storage::$last_error);
+ return false;
+ }
+
+ // create ID
+ return kolab_storage::folder_id($newfolder);
+ }
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // directly access storage object
+ if (!$this->events[$id] && ($record = $this->storage->get_object($id)))
+ $this->events[$id] = $this->_to_driver_event($record, true);
+
+ // event not found, maybe a recurring instance is requested
+ if (!$this->events[$id]) {
+ $master_id = preg_replace('/-\d+(T\d{6})?$/', '', $id);
+ $instance_id = substr($id, strlen($master_id) + 1);
+
+ if ($master_id != $id && ($record = $this->storage->get_object($master_id))) {
+ $master = $this->_to_driver_event($record);
+ }
+
+ // check for match in top-level exceptions (aka loose single occurrences)
+ if ($master && $master['_formatobj'] && ($instance = $master['_formatobj']->get_instance($instance_id))) {
+ $this->events[$id] = $this->_to_driver_event($instance);
+ }
+ // check for match on the first instance already
+ else if ($master['_instance'] && $master['_instance'] == $instance_id) {
+ $this->events[$id] = $master;
+ }
+ else if ($master && is_array($master['recurrence'])) {
+ $this->get_recurring_events($record, $master['start'], null, $id);
+ }
+ }
+
+ return $this->events[$id];
+ }
+
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!$this->ready)
+ return false;
+
+ $data = $this->storage->get_attachment($event['id'], $id);
+
+ if ($data == null) {
+ // try again with master UID
+ $uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
+ if ($uid != $event['id']) {
+ $data = $this->storage->get_attachment($uid, $id);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param boolean Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @param array Additional query to filter events
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array(), $filter_query = null)
+ {
+ // convert to DateTime for comparisons
+ try {
+ $start = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start = new DateTime('@0');
+ }
+ try {
+ $end = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end = new DateTime('today +10 years');
+ }
+
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+
+ // query Kolab storage
+ $query[] = array('dtstart', '<=', $end);
+ $query[] = array('dtend', '>=', $start);
+
+ if (is_array($filter_query)) {
+ $query = array_merge($query, $filter_query);
+ }
+
+ if (!empty($search)) {
+ $search = mb_strtolower($search);
+ $words = rcube_utils::tokenize_string($search, 1);
+ foreach (rcube_utils::normalize_string($search, true) as $word) {
+ $query[] = array('words', 'LIKE', $word);
+ }
+ }
+ else {
+ $words = array();
+ }
+
+ // set partstat filter to skip pending and declined invitations
+ if (empty($filter_query) && $this->get_namespace() != 'other') {
+ $partstat_exclude = array('NEEDS-ACTION','DECLINED');
+ }
+ else {
+ $partstat_exclude = array();
+ }
+
+ $events = array();
+ foreach ($this->storage->select($query) as $record) {
+ $event = $this->_to_driver_event($record, !$virtual);
+
+ // remember seen categories
+ if ($event['categories']) {
+ $cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
+ $this->categories[$cat]++;
+ }
+
+ // list events in requested time window
+ if ($event['start'] <= $end && $event['end'] >= $start) {
+ unset($event['_attendees']);
+ $add = true;
+
+ // skip the first instance of a recurring event if listed in exdate
+ if ($virtual && !empty($event['recurrence']['EXDATE'])) {
+ $event_date = $event['start']->format('Ymd');
+ $exdates = (array)$event['recurrence']['EXDATE'];
+
+ foreach ($exdates as $exdate) {
+ if ($exdate->format('Ymd') == $event_date) {
+ $add = false;
+ break;
+ }
+ }
+ }
+
+ // find and merge exception for the first instance
+ if ($virtual && !empty($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS'])) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
+ if ($event['_instance'] == $exception['_instance']) {
+ // clone date objects from main event before adjusting them with exception data
+ if (is_object($event['start'])) $event['start'] = clone $record['start'];
+ if (is_object($event['end'])) $event['end'] = clone $record['end'];
+ kolab_driver::merge_exception_data($event, $exception);
+ }
+ }
+ }
+
+ if ($add)
+ $events[] = $event;
+ }
+
+ // resolve recurring events
+ if ($record['recurrence'] && $virtual == 1) {
+ $events = array_merge($events, $this->get_recurring_events($record, $start, $end));
+ }
+ // add top-level exceptions (aka loose single occurrences)
+ else if (is_array($record['exceptions'])) {
+ foreach ($record['exceptions'] as $ex) {
+ $component = $this->_to_driver_event($ex);
+ if ($component['start'] <= $end && $component['end'] >= $start) {
+ $events[] = $component;
+ }
+ }
+ }
+ }
+
+ // post-filter all events by fulltext search and partstat values
+ $me = $this;
+ $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
+ // fulltext search
+ if (count($words)) {
+ $hits = 0;
+ foreach ($words as $word) {
+ $hits += $me->fulltext_match($event, $word, false);
+ }
+ if ($hits < count($words)) {
+ return false;
+ }
+ }
+
+ // partstat filter
+ if (count($partstat_exclude) && is_array($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $partstat_exclude)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ });
+
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->cal->rc->session->nowrite = true;
+
+ return $events;
+ }
+
+ /**
+ *
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @param array Additional query to filter events
+ * @return integer Count
+ */
+ public function count_events($start, $end = null, $filter_query = null)
+ {
+ // convert to DateTime for comparisons
+ try {
+ $start = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start = new DateTime('@0');
+ }
+ if ($end) {
+ try {
+ $end = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end = null;
+ }
+ }
+
+ // query Kolab storage
+ $query[] = array('dtend', '>=', $start);
+
+ if ($end)
+ $query[] = array('dtstart', '<=', $end);
+
+ // add query to exclude pending/declined invitations
+ if (empty($filter_query)) {
+ foreach ($this->cal->get_user_emails() as $email) {
+ $query[] = array('tags', '!=', 'x-partstat:' . $email . ':needs-action');
+ $query[] = array('tags', '!=', 'x-partstat:' . $email . ':declined');
+ }
+ }
+ else if (is_array($filter_query)) {
+ $query = array_merge($query, $filter_query);
+ }
+
+ // we rely the Kolab storage query (no post-filtering)
+ return $this->storage->count($query);
+ }
+
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ if (!is_array($event))
+ return false;
+
+ // email links are stored separately
+ $links = $event['links'];
+ unset($event['links']);
+
+ //generate new event from RC input
+ $object = $this->_from_driver_event($event);
+ $saved = $this->storage->save($object, 'event');
+
+ if (!$saved) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving event object to Kolab server"),
+ true, false);
+ $saved = false;
+ }
+ else {
+ // save links in configuration.relation object
+ $this->save_links($event['uid'], $links);
+
+ $this->events = array($event['uid'] => $this->_to_driver_event($object, true));
+ }
+
+ return $saved;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ * @return boolean True on success, False on error
+ */
+
+ public function update_event($event, $exception_id = null)
+ {
+ $updated = false;
+ $old = $this->storage->get_object($event['uid'] ?: $event['id']);
+ if (!$old || PEAR::isError($old))
+ return false;
+
+ // email links are stored separately
+ $links = $event['links'];
+ unset($event['links']);
+
+ $object = $this->_from_driver_event($event, $old);
+ $saved = $this->storage->save($object, 'event', $old['uid']);
+
+ if (!$saved) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error saving event object to Kolab server"),
+ true, false);
+ }
+ else {
+ // save links in configuration.relation object
+ $this->save_links($event['uid'], $links);
+
+ $updated = true;
+ $this->events = array($event['uid'] => $this->_to_driver_event($object, true));
+
+ // refresh local cache with recurring instances
+ if ($exception_id) {
+ $this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
+ }
+ }
+
+ return $updated;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ * @return boolean True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ $deleted = $this->storage->delete($event['uid'] ?: $event['id'], $force);
+
+ if (!$deleted) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => sprintf("Error deleting event object '%s' from Kolab server", $event['id'])),
+ true, false);
+ }
+
+ return $deleted;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ if ($this->storage->undelete($event['id'])) {
+ return true;
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error undeleting the event object $event[id] from the Kolab server"),
+ true, false);
+ }
+
+ return false;
+ }
+
+ /**
+ * Find messages linked with an event
+ */
+ protected function get_links($uid)
+ {
+ $storage = kolab_storage_config::get_instance();
+ return $storage->get_object_links($uid);
+ }
+
+ /**
+ *
+ */
+ protected function save_links($uid, $links)
+ {
+ // make sure we have a valid array
+ if (empty($links)) {
+ $links = array();
+ }
+
+ $storage = kolab_storage_config::get_instance();
+ $remove = array_diff($storage->get_object_links($uid), $links);
+ return $storage->save_object_links($uid, $links, $remove);
+ }
+
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array Hash array with event properties
+ * @param object DateTime Start date of the recurrence window
+ * @param object DateTime End date of the recurrence window
+ * @param string ID of a specific recurring event instance
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null, $event_id = null)
+ {
+ $object = $event['_formatobj'];
+ if (!$object) {
+ $rec = $this->storage->get_object($event['id']);
+ $object = $rec['_formatobj'];
+ }
+ if (!is_object($object))
+ return array();
+
+ // determine a reasonable end date if none given
+ if (!$end) {
+ switch ($event['recurrence']['FREQ']) {
+ case 'YEARLY': $intvl = 'P100Y'; break;
+ case 'MONTHLY': $intvl = 'P20Y'; break;
+ default: $intvl = 'P10Y'; break;
+ }
+
+ $end = clone $event['start'];
+ $end->add(new DateInterval($intvl));
+ }
+
+ // copy the recurrence rule from the master event (to be used in the UI)
+ $recurrence_rule = $event['recurrence'];
+ unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
+
+ // read recurrence exceptions first
+ $events = array();
+ $exdata = array();
+ $futuredata = array();
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+
+ if (is_array($event['recurrence']['EXCEPTIONS'])) {
+ foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
+ if (!$exception['_instance'])
+ $exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception);
+
+ $rec_event = $this->_to_driver_event($exception);
+ $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
+ $rec_event['isexception'] = 1;
+
+ // found the specifically requested instance: register exception (single occurrence wins)
+ if ($rec_event['id'] == $event_id && (!$this->events[$event_id] || $this->events[$event_id]['thisandfuture'])) {
+ $rec_event['recurrence'] = $recurrence_rule;
+ $rec_event['recurrence_id'] = $event['uid'];
+ $this->events[$rec_event['id']] = $rec_event;
+ }
+
+ // remember this exception's date
+ $exdate = substr($exception['_instance'], 0, 8);
+ if (!$exdata[$exdate] || $exdata[$exdate]['thisandfuture']) {
+ $exdata[$exdate] = $rec_event;
+ }
+ if ($rec_event['thisandfuture']) {
+ $futuredata[$exdate] = $rec_event;
+ }
+ }
+ }
+
+ // found the specifically requested instance, exiting...
+ if ($event_id && !empty($this->events[$event_id])) {
+ return array($this->events[$event_id]);
+ }
+
+ // use libkolab to compute recurring events
+ if (class_exists('kolabcalendaring')) {
+ $recurrence = new kolab_date_recurrence($object);
+ }
+ else {
+ // fallback to local recurrence implementation
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+ $recurrence = new calendar_recurrence($this->cal, $event);
+ }
+
+ $i = 0;
+ while ($next_event = $recurrence->next_instance()) {
+ $datestr = $next_event['start']->format('Ymd');
+ $instance_id = $next_event['start']->format($recurrence_id_format);
+
+ // use this event data for future recurring instances
+ if ($futuredata[$datestr])
+ $overlay_data = $futuredata[$datestr];
+
+ // add to output if in range
+ $rec_id = $event['uid'] . '-' . $instance_id;
+ if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) {
+ $rec_event = $this->_to_driver_event($next_event);
+ $rec_event['_instance'] = $instance_id;
+ $rec_event['_count'] = $i + 1;
+
+ if ($overlay_data || $exdata[$datestr]) // copy data from exception
+ kolab_driver::merge_exception_data($rec_event, $exdata[$datestr] ?: $overlay_data);
+
+ $rec_event['id'] = $rec_id;
+ $rec_event['recurrence_id'] = $event['uid'];
+ $rec_event['recurrence'] = $recurrence_rule;
+ unset($rec_event['_attendees']);
+ $events[] = $rec_event;
+
+ if ($rec_id == $event_id) {
+ $this->events[$rec_id] = $rec_event;
+ break;
+ }
+ }
+ else if ($next_event['start'] > $end) // stop loop if out of range
+ break;
+
+ // avoid endless recursion loops
+ if (++$i > 1000)
+ break;
+ }
+
+ return $events;
+ }
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ private function _to_driver_event($record, $noinst = false)
+ {
+ $record['calendar'] = $this->id;
+ $record['links'] = $this->get_links($record['uid']);
+
+ if ($this->get_namespace() == 'other') {
+ $record['className'] = 'fc-event-ns-other';
+ $record = kolab_driver::add_partstat_class($record, array('NEEDS-ACTION','DECLINED'), $this->get_owner());
+ }
+
+ // add instance identifier to first occurrence (master event)
+ $recurrence_id_format = libcalendaring::recurrence_id_format($record);
+ if (!$noinst && $record['recurrence'] && !$record['recurrence_id'] && !$record['_instance']) {
+ $record['_instance'] = $record['start']->format($recurrence_id_format);
+ }
+ else if (is_a($record['recurrence_date'], 'DateTime')) {
+ $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
+ }
+
+ // clean up exception data
+ if ($record['recurrence'] && is_array($record['recurrence']['EXCEPTIONS'])) {
+ array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
+ unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
+ });
+ }
+
+ return $record;
+ }
+
+ /**
+ * Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
+ * (opposite of self::_to_driver_event())
+ */
+ private function _from_driver_event($event, $old = array())
+ {
+ // set current user as ORGANIZER
+ $identity = $this->cal->rc->user->list_emails(true);
+ if (empty($event['attendees']) && $identity['email'])
+ $event['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
+
+ $event['_owner'] = $identity['email'];
+
+ // remove EXDATE values if RDATE is given
+ if (!empty($event['recurrence']['RDATE'])) {
+ $event['recurrence']['EXDATE'] = array();
+ }
+
+ // remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
+ if ($event['recurrence'] && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
+ $event['recurrence'] = array();
+ }
+
+ // keep 'comment' from initial itip invitation
+ if (!empty($old['comment'])) {
+ $event['comment'] = $old['comment'];
+ }
+
+ // clean up exception data
+ if (is_array($event['exceptions'])) {
+ array_walk($event['exceptions'], function(&$exception) {
+ unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments'],
+ $event['attachments'], $event['deleted_attachments'], $event['recurrence_id']);
+ });
+ }
+
+
+ // remove some internal properties which should not be saved
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
+ $event['recurrence_id'], $event['attachments'], $event['deleted_attachments'], $event['className']);
+
+ // copy meta data (starting with _) from old object
+ foreach ((array)$old as $key => $val) {
+ if (!isset($event[$key]) && $key[0] == '_')
+ $event[$key] = $val;
+ }
+
+ return $event;
+ }
+
+ /**
+ * Match the given word in the event contents
+ */
+ public function fulltext_match($event, $word, $recursive = true)
+ {
+ $hits = 0;
+ foreach ($this->search_fields as $col) {
+ $sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
+ if (empty($sval))
+ continue;
+
+ // do a simple substring matching (to be improved)
+ $val = mb_strtolower($sval);
+ if (strpos($val, $word) !== false) {
+ $hits++;
+ break;
+ }
+ }
+
+ return $hits;
+ }
+
+ /**
+ * Convert a complex event attribute to a string value
+ */
+ private static function _complex2string($prop)
+ {
+ static $ignorekeys = array('role','status','rsvp');
+
+ $out = '';
+ if (is_array($prop)) {
+ foreach ($prop as $key => $val) {
+ if (is_numeric($key)) {
+ $out .= self::_complex2string($val);
+ }
+ else if (!in_array($key, $ignorekeys)) {
+ $out .= $val . ' ';
+ }
+ }
+ }
+ else if (is_string($prop) || is_numeric($prop)) {
+ $out .= $prop . ' ';
+ }
+
+ return rtrim($out);
+ }
+
+}
diff --git a/calendar/drivers/kolab/kolab_driver.php b/calendar/drivers/kolab/kolab_driver.php
new file mode 100644
index 0000000..d4f9a19
--- /dev/null
+++ b/calendar/drivers/kolab/kolab_driver.php
@@ -0,0 +1,2526 @@
+<?php
+
+/**
+ * Kolab driver for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_driver extends calendar_driver
+{
+ const INVITATIONS_CALENDAR_PENDING = '--invitation--pending';
+ const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
+
+ // features this backend supports
+ public $alarms = true;
+ public $attendees = true;
+ public $freebusy = true;
+ public $attachments = true;
+ public $undelete = true;
+ public $alarm_types = array('DISPLAY','AUDIO');
+ public $categoriesimmutable = true;
+
+ private $rc;
+ private $cal;
+ private $calendars;
+ private $has_writeable = false;
+ private $freebusy_trigger = false;
+ private $bonnie_api = false;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($cal)
+ {
+ $cal->require_plugin('libkolab');
+
+ // load helper classes *after* libkolab has been loaded (#3248)
+ require_once(dirname(__FILE__) . '/kolab_calendar.php');
+ require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
+ require_once(dirname(__FILE__) . '/kolab_invitation_calendar.php');
+
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->_read_calendars();
+
+ $this->cal->register_action('push-freebusy', array($this, 'push_freebusy'));
+ $this->cal->register_action('calendar-acl', array($this, 'calendar_acl'));
+
+ $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
+
+ if (kolab_storage::$version == '2.0') {
+ $this->alarm_types = array('DISPLAY');
+ $this->alarm_absolute = false;
+ }
+
+ // get configuration for the Bonnie API
+ if ($bonnie_config = $this->cal->rc->config->get('kolab_bonnie_api', false))
+ $this->bonnie_api = new kolab_bonnie_api($bonnie_config);
+
+ // calendar uses fully encoded identifiers
+ kolab_storage::$encode_ids = true;
+ }
+
+
+ /**
+ * Read available calendars from server
+ */
+ private function _read_calendars()
+ {
+ // already read sources
+ if (isset($this->calendars))
+ return $this->calendars;
+
+ // get all folders that have "event" type, sorted by namespace/name
+ $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
+ $this->calendars = array();
+
+ foreach ($folders as $folder) {
+ if ($folder instanceof kolab_storage_folder_user) {
+ $calendar = new kolab_user_calendar($folder->name, $this->cal);
+ $calendar->subscriptions = count($folder->children) > 0;
+ }
+ else {
+ $calendar = new kolab_calendar($folder->name, $this->cal);
+ }
+
+ if ($calendar->ready) {
+ $this->calendars[$calendar->id] = $calendar;
+ if ($calendar->editable)
+ $this->has_writeable = true;
+ }
+ }
+
+ return $this->calendars;
+ }
+
+ /**
+ * Get a list of available calendars from this source
+ *
+ * @param integer $filter Bitmask defining filter criterias
+ * @param object $tree Reference to hierarchical folder tree object
+ *
+ * @return array List of calendars
+ */
+ public function list_calendars($filter = 0, &$tree = null)
+ {
+ // attempt to create a default calendar for this user
+ if (!$this->has_writeable) {
+ if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
+ unset($this->calendars);
+ $this->_read_calendars();
+ }
+ }
+
+ $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+ $folders = $this->filter_calendars($filter);
+ $calendars = array();
+
+ // include virtual folders for a full folder tree
+ if (!is_null($tree))
+ $folders = kolab_storage::folder_hierarchy($folders, $tree);
+
+ foreach ($folders as $id => $cal) {
+ $fullname = $cal->get_name();
+ $listname = $cal->get_foldername();
+ $imap_path = explode($delim, $cal->name);
+
+ // find parent
+ do {
+ array_pop($imap_path);
+ $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+ }
+ while (count($imap_path) > 1 && !$this->calendars[$parent_id]);
+
+ // restore "real" parent ID
+ if ($parent_id && !$this->calendars[$parent_id]) {
+ $parent_id = kolab_storage::folder_id($cal->get_parent());
+ }
+
+ // turn a kolab_storage_folder object into a kolab_calendar
+ if ($cal instanceof kolab_storage_folder) {
+ $cal = new kolab_calendar($cal->name, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ }
+
+ // special handling for user or virtual folders
+ if ($cal instanceof kolab_storage_folder_user) {
+ $calendars[$cal->id] = array(
+ 'id' => $cal->id,
+ 'name' => kolab_storage::object_name($fullname),
+ 'listname' => $listname,
+ 'editname' => $cal->get_foldername(),
+ 'color' => $cal->get_color(),
+ 'active' => $cal->is_active(),
+ 'title' => $cal->get_owner(),
+ 'owner' => $cal->get_owner(),
+ 'history' => false,
+ 'virtual' => false,
+ 'editable' => false,
+ 'group' => 'other',
+ 'class' => 'user',
+ 'removable' => true,
+ );
+ }
+ else if ($cal->virtual) {
+ $calendars[$cal->id] = array(
+ 'id' => $cal->id,
+ 'name' => $fullname,
+ 'listname' => $listname,
+ 'editname' => $cal->get_foldername(),
+ 'virtual' => true,
+ 'editable' => false,
+ 'group' => $cal->get_namespace(),
+ 'class' => 'folder',
+ );
+ }
+ else {
+ $calendars[$cal->id] = array(
+ 'id' => $cal->id,
+ 'name' => $fullname,
+ 'listname' => $listname,
+ 'editname' => $cal->get_foldername(),
+ 'title' => $cal->get_title(),
+ 'color' => $cal->get_color(),
+ 'editable' => $cal->editable,
+ 'rights' => $cal->rights,
+ 'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
+ 'group' => $cal->get_namespace(),
+ 'default' => $cal->default,
+ 'active' => $cal->is_active(),
+ 'owner' => $cal->get_owner(),
+ 'children' => true, // TODO: determine if that folder indeed has child folders
+ 'parent' => $parent_id,
+ 'subtype' => $cal->subtype,
+ 'caldavurl' => $cal->get_caldav_url(),
+ 'removable' => !$cal->default,
+ );
+ }
+
+ if ($cal->subscriptions) {
+ $calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
+ }
+ }
+
+ // list virtual calendars showing invitations
+ if ($this->rc->config->get('kolab_invitation_calendars')) {
+ foreach (array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED) as $id) {
+ $cal = new kolab_invitation_calendar($id, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
+ $calendars[$id] = array(
+ 'id' => $cal->id,
+ 'name' => $cal->get_name(),
+ 'listname' => $cal->get_name(),
+ 'editname' => $cal->get_foldername(),
+ 'title' => $cal->get_title(),
+ 'color' => $cal->get_color(),
+ 'editable' => $cal->editable,
+ 'rights' => $cal->rights,
+ 'showalarms' => $cal->alarms,
+ 'history' => !empty($this->bonnie_api),
+ 'group' => 'x-invitations',
+ 'default' => false,
+ 'active' => $cal->is_active(),
+ 'owner' => $cal->get_owner(),
+ 'children' => false,
+ );
+
+ if ($id == self::INVITATIONS_CALENDAR_PENDING) {
+ $calendars[$id]['counts'] = true;
+ }
+
+ if (is_object($tree)) {
+ $tree->children[] = $cal;
+ }
+ }
+ }
+ }
+
+ // append the virtual birthdays calendar
+ if ($this->rc->config->get('calendar_contact_birthdays', false)) {
+ $id = self::BIRTHDAY_CALENDAR_ID;
+ $prefs = $this->rc->config->get('kolab_calendars', array()); // read local prefs
+ if (!($filter & self::FILTER_ACTIVE) || $prefs[$id]['active']) {
+ $calendars[$id] = array(
+ 'id' => $id,
+ 'name' => $this->cal->gettext('birthdays'),
+ 'listname' => $this->cal->gettext('birthdays'),
+ 'color' => $prefs[$id]['color'] ?: '87CEFA',
+ 'active' => (bool)$prefs[$id]['active'],
+ 'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
+ 'group' => 'x-birthdays',
+ 'editable' => false,
+ 'default' => false,
+ 'children' => false,
+ 'history' => false,
+ );
+ }
+ }
+
+ return $calendars;
+ }
+
+ /**
+ * Get list of calendars according to specified filters
+ *
+ * @param integer Bitmask defining restrictions. See FILTER_* constants for possible values.
+ *
+ * @return array List of calendars
+ */
+ protected function filter_calendars($filter)
+ {
+ $calendars = array();
+
+ $plugin = $this->rc->plugins->exec_hook('calendar_list_filter', array(
+ 'list' => $this->calendars,
+ 'calendars' => $calendars,
+ 'filter' => $filter,
+ 'editable' => ($filter & self::FILTER_WRITEABLE),
+ 'insert' => ($filter & self::FILTER_INSERTABLE),
+ 'active' => ($filter & self::FILTER_ACTIVE),
+ 'personal' => ($filter & self::FILTER_PERSONAL)
+ ));
+
+ if ($plugin['abort']) {
+ return $plugin['calendars'];
+ }
+
+ foreach ($this->calendars as $cal) {
+ if (!$cal->ready) {
+ continue;
+ }
+ if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) {
+ continue;
+ }
+ if (($filter & self::FILTER_INSERTABLE) && !$cal->insert) {
+ continue;
+ }
+ if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) {
+ continue;
+ }
+ if (($filter & self::FILTER_PRIVATE) && $cal->subtype != 'private') {
+ continue;
+ }
+ if (($filter & self::FILTER_CONFIDENTIAL) && $cal->subtype != 'confidential') {
+ continue;
+ }
+ if (($filter & self::FILTER_PERSONAL) && $cal->get_namespace() != 'personal') {
+ continue;
+ }
+ $calendars[$cal->id] = $cal;
+ }
+
+ return $calendars;
+ }
+
+
+ /**
+ * Get the kolab_calendar instance for the given calendar ID
+ *
+ * @param string Calendar identifier (encoded imap folder name)
+ * @return object kolab_calendar Object nor null if calendar doesn't exist
+ */
+ public function get_calendar($id)
+ {
+ // create calendar object if necesary
+ if (!$this->calendars[$id] && in_array($id, array(self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
+ $this->calendars[$id] = new kolab_invitation_calendar($id, $this->cal);
+ }
+ else if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
+ $calendar = kolab_calendar::factory($id, $this->cal);
+ if ($calendar->ready)
+ $this->calendars[$calendar->id] = $calendar;
+ }
+
+ return $this->calendars[$id];
+ }
+
+ /**
+ * Create a new calendar assigned to the current user
+ *
+ * @param array Hash array with calendar properties
+ * name: Calendar name
+ * color: The color of the calendar
+ * @return mixed ID of the calendar on success, False on error
+ */
+ public function create_calendar($prop)
+ {
+ $prop['type'] = 'event';
+ $prop['active'] = true;
+ $prop['subscribed'] = true;
+ $folder = kolab_storage::folder_update($prop);
+
+ if ($folder === false) {
+ $this->last_error = $this->cal->gettext(kolab_storage::$last_error);
+ return false;
+ }
+
+ // create ID
+ $id = kolab_storage::folder_id($folder);
+
+ // save color in user prefs (temp. solution)
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
+
+ if (isset($prop['color']))
+ $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
+ if (isset($prop['showalarms']))
+ $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
+
+ if ($prefs['kolab_calendars'][$id])
+ $this->rc->user->save_prefs($prefs);
+
+ return $id;
+ }
+
+
+ /**
+ * Update properties of an existing calendar
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function edit_calendar($prop)
+ {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
+ $id = $cal->update($prop);
+ }
+ else {
+ $id = $prop['id'];
+ }
+
+ // fallback to local prefs
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
+ unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']);
+
+ if (isset($prop['color']))
+ $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
+
+ if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID)
+ $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
+ else if (isset($prop['showalarms']))
+ $prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
+
+ if (!empty($prefs['kolab_calendars'][$id]))
+ $this->rc->user->save_prefs($prefs);
+
+ return true;
+ }
+
+
+ /**
+ * Set active/subscribed state of a calendar
+ *
+ * @see calendar_driver::subscribe_calendar()
+ */
+ public function subscribe_calendar($prop)
+ {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id'])) && is_object($cal->storage)) {
+ $ret = false;
+ if (isset($prop['permanent']))
+ $ret |= $cal->storage->subscribe(intval($prop['permanent']));
+ if (isset($prop['active']))
+ $ret |= $cal->storage->activate(intval($prop['active']));
+
+ // apply to child folders, too
+ if ($prop['recursive']) {
+ foreach ((array)kolab_storage::list_folders($cal->storage->name, '*', 'event') as $subfolder) {
+ if (isset($prop['permanent']))
+ ($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
+ if (isset($prop['active']))
+ ($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
+ }
+ }
+ return $ret;
+ }
+ else {
+ // save state in local prefs
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
+ $prefs['kolab_calendars'][$prop['id']]['active'] = (bool)$prop['active'];
+ $this->rc->user->save_prefs($prefs);
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Delete the given calendar with all its contents
+ *
+ * @see calendar_driver::delete_calendar()
+ */
+ public function delete_calendar($prop)
+ {
+ if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
+ $folder = $cal->get_realname();
+ // TODO: unsubscribe if no admin rights
+ if (kolab_storage::folder_delete($folder)) {
+ // remove color in user prefs (temp. solution)
+ $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
+ unset($prefs['kolab_calendars'][$prop['id']]);
+
+ $this->rc->user->save_prefs($prefs);
+ return true;
+ }
+ else
+ $this->last_error = kolab_storage::$last_error;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Search for shared or otherwise not listed calendars the user has access
+ *
+ * @param string Search string
+ * @param string Section/source to search
+ * @return array List of calendars
+ */
+ public function search_calendars($query, $source)
+ {
+ if (!kolab_storage::setup())
+ return array();
+
+ $this->calendars = array();
+ $this->search_more_results = false;
+
+ // find unsubscribed IMAP folders that have "event" type
+ if ($source == 'folders') {
+ foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
+ $calendar = new kolab_calendar($folder->name, $this->cal);
+ $this->calendars[$calendar->id] = $calendar;
+ }
+ }
+ // find other user's virtual calendars
+ else if ($source == 'users') {
+ $limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
+ foreach (kolab_storage::search_users($query, 0, array(), $limit, $count) as $user) {
+ $calendar = new kolab_user_calendar($user, $this->cal);
+ $this->calendars[$calendar->id] = $calendar;
+
+ // search for calendar folders shared by this user
+ foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ $this->calendars[$cal->id] = $cal;
+ $calendar->subscriptions = true;
+ }
+ }
+
+ if ($count > $limit) {
+ $this->search_more_results = true;
+ }
+ }
+
+ // don't list the birthday calendar
+ $this->rc->config->set('calendar_contact_birthdays', false);
+ $this->rc->config->set('kolab_invitation_calendars', false);
+
+ return $this->list_calendars();
+ }
+
+
+ /**
+ * Fetch a single event
+ *
+ * @see calendar_driver::get_event()
+ * @return array Hash array with event properties, false if not found
+ */
+ public function get_event($event, $scope = 0, $full = false)
+ {
+ if (is_array($event)) {
+ $id = $event['id'] ?: $event['uid'];
+ $cal = $event['calendar'];
+
+ // we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
+ if (!$event['id'] && $event['_instance']) {
+ $id .= '-' . $event['_instance'];
+ }
+ }
+ else {
+ $id = $event;
+ }
+
+ if ($cal) {
+ if ($storage = $this->get_calendar($cal)) {
+ $result = $storage->get_event($id);
+ return self::to_rcube_event($result);
+ }
+ // get event from the address books birthday calendar
+ else if ($cal == self::BIRTHDAY_CALENDAR_ID) {
+ return $this->get_birthday_event($id);
+ }
+ }
+ // iterate over all calendar folders and search for the event ID
+ else {
+ foreach ($this->filter_calendars($scope) as $calendar) {
+ if ($result = $calendar->get_event($id)) {
+ return self::to_rcube_event($result);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Add a single event to the database
+ *
+ * @see calendar_driver::new_event()
+ */
+ public function new_event($event)
+ {
+ if (!$this->validate($event))
+ return false;
+
+ $event = self::from_rcube_event($event);
+
+ $cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
+ if ($storage = $this->get_calendar($cid)) {
+ // if this is a recurrence instance, append as exception to an already existing object for this UID
+ if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
+ self::add_exception($master, $event);
+ $success = $storage->update_event($master);
+ }
+ else {
+ $success = $storage->insert_event($event);
+ }
+
+ if ($success && $this->freebusy_trigger) {
+ $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
+ $this->freebusy_trigger = false; // disable after first execution (#2355)
+ }
+
+ return $success;
+ }
+
+ return false;
+ }
+
+ /**
+ * Update an event entry with the given data
+ *
+ * @see calendar_driver::new_event()
+ * @return boolean True on success, False on error
+ */
+ public function edit_event($event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar'])))
+ return false;
+
+ return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
+ }
+
+ /**
+ * Extended event editing with possible changes to the argument
+ *
+ * @param array Hash array with event properties
+ * @param string New participant status
+ * @param array List of hash arrays with updated attendees
+ * @return boolean True on success, False on error
+ */
+ public function edit_rsvp(&$event, $status, $attendees)
+ {
+ $update_event = $event;
+
+ // apply changes to master (and all exceptions)
+ if ($event['_savemode'] == 'all' && $event['recurrence_id']) {
+ if ($storage = $this->get_calendar($event['calendar'])) {
+ $update_event = $storage->get_event($event['recurrence_id']);
+ $update_event['_savemode'] = $event['_savemode'];
+ $update_event['id'] = $update_event['uid'];
+ unset($update_event['recurrence_id']);
+ calendar::merge_attendee_data($update_event, $attendees);
+ }
+ }
+
+ if ($ret = $this->update_attendees($update_event, $attendees)) {
+ // replace with master event (for iTip reply)
+ $event = self::to_rcube_event($update_event);
+
+ // re-assign to the according (virtual) calendar
+ if ($this->rc->config->get('kolab_invitation_calendars')) {
+ if (strtoupper($status) == 'DECLINED')
+ $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
+ else if (strtoupper($status) == 'NEEDS-ACTION')
+ $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
+ else if ($event['_folder_id'])
+ $event['calendar'] = $event['_folder_id'];
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Update the participant status for the given attendees
+ *
+ * @see calendar_driver::update_attendees()
+ */
+ public function update_attendees(&$event, $attendees)
+ {
+ // for this-and-future updates, merge the updated attendees onto all exceptions in range
+ if (($event['_savemode'] == 'future' && $event['recurrence_id']) || (!empty($event['recurrence']) && !$event['recurrence_id'])) {
+ if (!($storage = $this->get_calendar($event['calendar'])))
+ return false;
+
+ // load master event
+ $master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
+
+ // apply attendee update to each existing exception
+ if ($master['recurrence'] && !empty($master['recurrence']['EXCEPTIONS'])) {
+ $saved = false;
+ foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ // merge the new event properties onto future exceptions
+ if ($exception['_instance'] >= strval($event['_instance'])) {
+ calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
+ }
+ // update a specific instance
+ if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
+ $saved = true;
+ }
+ }
+
+ // add the given event as new exception
+ if (!$saved && $event['id'] != $master['id']) {
+ $event['thisandfuture'] = true;
+ $master['recurrence']['EXCEPTIONS'][] = $event;
+ }
+
+ // set link to top-level exceptions
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+
+ return $this->update_event($master);
+ }
+ }
+
+ // just update the given event (instance)
+ return $this->update_event($event);
+ }
+
+ /**
+ * Move a single event
+ *
+ * @see calendar_driver::move_event()
+ * @return boolean True on success, False on error
+ */
+ public function move_event($event)
+ {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
+ unset($ev['sequence']);
+ self::clear_attandee_noreply($ev);
+ return $this->update_event($event + $ev);
+ }
+
+ return false;
+ }
+
+ /**
+ * Resize a single event
+ *
+ * @see calendar_driver::resize_event()
+ * @return boolean True on success, False on error
+ */
+ public function resize_event($event)
+ {
+ if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
+ unset($ev['sequence']);
+ self::clear_attandee_noreply($ev);
+ return $this->update_event($event + $ev);
+ }
+
+ return false;
+ }
+
+ /**
+ * Remove a single event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * @param boolean Remove record(s) irreversible (mark as deleted otherwise)
+ *
+ * @return boolean True on success, False on error
+ */
+ public function remove_event($event, $force = true)
+ {
+ $ret = true;
+ $success = false;
+ $savemode = $event['_savemode'];
+ $decline = $event['_decline'];
+
+ if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
+ $event['_savemode'] = $savemode;
+ $savemode = 'all';
+ $master = $event;
+
+ $this->rc->session->remove('calendar_restore_event_data');
+
+ // read master if deleting a recurring event
+ if ($event['recurrence'] || $event['recurrence_id'] || $event['isexception']) {
+ $master = $storage->get_event($event['uid']);
+ $savemode = $event['_savemode'] ?: ($event['_instance'] || $event['isexception'] ? 'current' : 'all');
+
+ // force 'current' mode for single occurrences stored as exception
+ if (!$event['recurrence'] && !$event['recurrence_id'] && $event['isexception'])
+ $savemode = 'current';
+ }
+
+ // removing an exception instance
+ if (($event['recurrence_id'] || $event['isexception']) && is_array($master['exceptions'])) {
+ foreach ($master['exceptions'] as $i => $exception) {
+ if ($exception['_instance'] == $event['_instance']) {
+ unset($master['exceptions'][$i]);
+ // set event date back to the actual occurrence
+ if ($exception['recurrence_date'])
+ $event['start'] = $exception['recurrence_date'];
+ }
+ }
+
+ if (is_array($master['recurrence'])) {
+ $master['recurrence']['EXCEPTIONS'] = &$master['exceptions'];
+ }
+ }
+
+ switch ($savemode) {
+ case 'current':
+ $_SESSION['calendar_restore_event_data'] = $master;
+
+ // removing the first instance => just move to next occurence
+ if ($master['recurrence'] && $event['_instance'] == libcalendaring::recurrence_instance_identifier($master)) {
+ $recurring = reset($storage->get_recurring_events($event, $event['start'], null, $event['id'].'-1'));
+
+ // no future instances found: delete the master event (bug #1677)
+ if (!$recurring['start']) {
+ $success = $storage->delete_event($master, $force);
+ break;
+ }
+
+ $master['start'] = $recurring['start'];
+ $master['end'] = $recurring['end'];
+ if ($master['recurrence']['COUNT'])
+ $master['recurrence']['COUNT']--;
+ }
+ // remove the matching RDATE entry
+ else if ($master['recurrence']['RDATE']) {
+ foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
+ unset($master['recurrence']['RDATE'][$j]);
+ break;
+ }
+ }
+ }
+ else { // add exception to master event
+ $master['recurrence']['EXDATE'][] = $event['start'];
+ }
+ $success = $storage->update_event($master);
+ break;
+
+ case 'future':
+ $master['_instance'] = libcalendaring::recurrence_instance_identifier($master);
+ if ($master['_instance'] != $event['_instance']) {
+ $_SESSION['calendar_restore_event_data'] = $master;
+
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $event['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+
+ // if all future instances are deleted, remove recurrence rule entirely (bug #1677)
+ if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
+ $master['recurrence'] = array();
+ }
+ // remove matching RDATE entries
+ else if ($master['recurrence']['RDATE']) {
+ foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
+ $master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
+ break;
+ }
+ }
+ }
+
+ $success = $storage->update_event($master);
+ $ret = $master['uid'];
+ break;
+ }
+
+ default: // 'all' is default
+ // removing the master event with loose exceptions (not recurring though)
+ if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
+ // make the first exception the new master
+ $newmaster = array_shift($master['exceptions']);
+ $newmaster['exceptions'] = $master['exceptions'];
+ $newmaster['_attachments'] = $master['_attachments'];
+ $newmaster['_mailbox'] = $master['_mailbox'];
+ $newmaster['_msguid'] = $master['_msguid'];
+
+ $success = $storage->update_event($newmaster);
+ }
+ else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
+ // don't delete but set PARTSTAT=DECLINED
+ if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
+ $success = $storage->update_event($master);
+ }
+ }
+
+ if (!$success)
+ $success = $storage->delete_event($master, $force);
+ break;
+ }
+ }
+
+ if ($success && $this->freebusy_trigger)
+ $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
+
+ return $success ? $ret : false;
+ }
+
+ /**
+ * Restore a single deleted event
+ *
+ * @param array Hash array with event properties:
+ * id: Event identifier
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ if ($storage = $this->get_calendar($event['calendar'])) {
+ if (!empty($_SESSION['calendar_restore_event_data']))
+ $success = $storage->update_event($_SESSION['calendar_restore_event_data']);
+ else
+ $success = $storage->restore_event($event);
+
+ if ($success && $this->freebusy_trigger)
+ $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
+
+ return $success;
+ }
+
+ return false;
+ }
+
+ /**
+ * Wrapper to update an event object depending on the given savemode
+ */
+ private function update_event($event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar'])))
+ return false;
+
+ // move event to another folder/calendar
+ if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
+ if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
+ return false;
+
+ $old = $fromcalendar->get_event($event['id']);
+
+ if ($event['_savemode'] != 'new') {
+ if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
+ return false;
+ }
+
+ $fromcalendar = $storage;
+ }
+ }
+ else
+ $fromcalendar = $storage;
+
+ $success = false;
+ $savemode = 'all';
+ $attachments = array();
+ $old = $master = $storage->get_event($event['id']);
+
+ if (!$old || !$old['start']) {
+ rcube::raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to load event object to update: id=" . $event['id']),
+ true, false);
+ return false;
+ }
+
+ // modify a recurring event, check submitted savemode to do the right things
+ if ($old['recurrence'] || $old['recurrence_id'] || $old['isexception']) {
+ $master = $storage->get_event($old['uid']);
+ $savemode = $event['_savemode'] ?: ($old['recurrence_id'] || $old['isexception'] ? 'current' : 'all');
+
+ // this-and-future on the first instance equals to 'all'
+ if ($savemode == 'future' && $master['start'] && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master))
+ $savemode = 'all';
+ // force 'current' mode for single occurrences stored as exception
+ else if (!$old['recurrence'] && !$old['recurrence_id'] && $old['isexception'])
+ $savemode = 'current';
+ }
+
+ // check if update affects scheduling and update attendee status accordingly
+ $reschedule = $this->check_scheduling($event, $old, true);
+
+ // keep saved exceptions (not submitted by the client)
+ if ($old['recurrence']['EXDATE'] && !isset($event['recurrence']['EXDATE']))
+ $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
+ if (isset($event['recurrence']['EXCEPTIONS']))
+ $with_exceptions = true; // exceptions already provided (e.g. from iCal import)
+ else if ($old['recurrence']['EXCEPTIONS'])
+ $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
+ else if ($old['exceptions'])
+ $event['exceptions'] = $old['exceptions'];
+
+ // remove some internal properties which should not be saved
+ unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
+ $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']);
+
+ switch ($savemode) {
+ case 'new':
+ // save submitted data as new (non-recurring) event
+ $event['recurrence'] = array();
+ $event['_copyfrom'] = $master['_msguid'];
+ $event['_mailbox'] = $master['_mailbox'];
+ $event['uid'] = $this->cal->generate_uid();
+ unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
+
+ // copy attachment metadata to new event
+ $event = self::from_rcube_event($event, $master);
+
+ self::clear_attandee_noreply($event);
+ if ($success = $storage->insert_event($event))
+ $success = $event['uid'];
+ break;
+
+ case 'future':
+ // create a new recurring event
+ $event['_copyfrom'] = $master['_msguid'];
+ $event['_mailbox'] = $master['_mailbox'];
+ $event['uid'] = $this->cal->generate_uid();
+ unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
+
+ // copy attachment metadata to new event
+ $event = self::from_rcube_event($event, $master);
+
+ // remove recurrence exceptions on re-scheduling
+ if ($reschedule) {
+ unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
+ }
+ else if (is_array($event['recurrence']['EXCEPTIONS'])) {
+ // only keep relevant exceptions
+ $event['recurrence']['EXCEPTIONS'] = array_filter($event['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
+ return $exception['start'] > $event['start'];
+ });
+ if (is_array($event['recurrence']['EXDATE'])) {
+ $event['recurrence']['EXDATE'] = array_filter($event['recurrence']['EXDATE'], function($exdate) use ($event) {
+ return $exdate > $event['start'];
+ });
+ }
+ // set link to top-level exceptions
+ $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+ }
+
+ // compute remaining occurrences
+ if ($event['recurrence']['COUNT']) {
+ if (!$old['_count'])
+ $old['_count'] = $this->get_recurrence_count($master, $old['start']);
+ $event['recurrence']['COUNT'] -= intval($old['_count']);
+ }
+
+ // remove fixed weekday when date changed
+ if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) {
+ if (strlen($event['recurrence']['BYDAY']) == 2)
+ unset($event['recurrence']['BYDAY']);
+ if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
+ unset($event['recurrence']['BYMONTH']);
+ }
+
+ // set until-date on master event
+ $master['recurrence']['UNTIL'] = clone $old['start'];
+ $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
+ unset($master['recurrence']['COUNT']);
+
+ // remove all exceptions after $event['start']
+ if (is_array($master['recurrence']['EXCEPTIONS'])) {
+ $master['recurrence']['EXCEPTIONS'] = array_filter($master['recurrence']['EXCEPTIONS'], function($exception) use ($event) {
+ return $exception['start'] < $event['start'];
+ });
+ // set link to top-level exceptions
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+ }
+ if (is_array($master['recurrence']['EXDATE'])) {
+ $master['recurrence']['EXDATE'] = array_filter($master['recurrence']['EXDATE'], function($exdate) use ($event) {
+ return $exdate < $event['start'];
+ });
+ }
+
+ // save new event
+ if ($success = $storage->insert_event($event)) {
+ $success = $event['uid'];
+
+ // update master event (no rescheduling!)
+ self::clear_attandee_noreply($master);
+ $storage->update_event($master);
+ }
+ break;
+
+ case 'current':
+ // recurring instances shall not store recurrence rules and attachments
+ $event['recurrence'] = array();
+ $event['thisandfuture'] = $savemode == 'future';
+ unset($event['attachments'], $event['id']);
+
+ // increment sequence of this instance if scheduling is affected
+ if ($reschedule) {
+ $event['sequence'] = max($old['sequence'], $master['sequence']) + 1;
+ }
+ else if (!isset($event['sequence'])) {
+ $event['sequence'] = $old['sequence'] ?: $master['sequence'];
+ }
+
+ // save properties to a recurrence exception instance
+ if ($old['_instance'] && is_array($master['recurrence']['EXCEPTIONS'])) {
+ if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
+ $success = $storage->update_event($master, $old['id']);
+ break;
+ }
+ }
+
+ $add_exception = true;
+
+ // adjust matching RDATE entry if dates changed
+ if (is_array($master['recurrence']['RDATE']) && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')) {
+ foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $old_date) {
+ $master['recurrence']['RDATE'][$j] = $event['start'];
+ sort($master['recurrence']['RDATE']);
+ $add_exception = false;
+ break;
+ }
+ }
+ }
+
+ // save as new exception to master event
+ if ($add_exception) {
+ self::add_exception($master, $event, $old);
+ }
+
+ $success = $storage->update_event($master);
+ break;
+
+ default: // 'all' is default
+ $event['id'] = $master['uid'];
+ $event['uid'] = $master['uid'];
+
+ // use start date from master but try to be smart on time or duration changes
+ $old_start_date = $old['start']->format('Y-m-d');
+ $old_start_time = $old['allday'] ? '' : $old['start']->format('H:i');
+ $old_duration = $old['end']->format('U') - $old['start']->format('U');
+
+ $new_start_date = $event['start']->format('Y-m-d');
+ $new_start_time = $event['allday'] ? '' : $event['start']->format('H:i');
+ $new_duration = $event['end']->format('U') - $event['start']->format('U');
+
+ $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
+ $date_shift = $old['start']->diff($event['start']);
+
+ // shifted or resized
+ if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
+ $event['start'] = $master['start']->add($date_shift);
+ $event['end'] = clone $event['start'];
+ $event['end']->add(new DateInterval('PT'.$new_duration.'S'));
+
+ // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
+ if ($old_start_date != $new_start_date) {
+ if (strlen($event['recurrence']['BYDAY']) == 2)
+ unset($event['recurrence']['BYDAY']);
+ if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
+ unset($event['recurrence']['BYMONTH']);
+ }
+ }
+ // dates did not change, use the ones from master
+ else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
+ $event['start'] = $master['start'];
+ $event['end'] = $master['end'];
+ }
+
+ // when saving an instance in 'all' mode, copy recurrence exceptions over
+ if ($old['recurrence_id']) {
+ $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'];
+ }
+ else if ($master['_instance']) {
+ $event['_instance'] = $master['_instance'];
+ $event['recurrence_date'] = $master['recurrence_date'];
+ }
+
+ // TODO: forward changes to exceptions (which do not yet have differing values stored)
+ if (is_array($event['recurrence']) && is_array($event['recurrence']['EXCEPTIONS']) && !$with_exceptions) {
+ // determine added and removed attendees
+ $old_attendees = $current_attendees = $added_attendees = array();
+ foreach ((array)$old['attendees'] as $attendee) {
+ $old_attendees[] = $attendee['email'];
+ }
+ foreach ((array)$event['attendees'] as $attendee) {
+ $current_attendees[] = $attendee['email'];
+ if (!in_array($attendee['email'], $old_attendees)) {
+ $added_attendees[] = $attendee;
+ }
+ }
+ $removed_attendees = array_diff($old_attendees, $current_attendees);
+
+ foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
+ }
+
+ // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
+ if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
+ $recurrence_id_format = libcalendaring::recurrence_id_format($event);
+ foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ $recurrence_id = is_a($exception['recurrence_date'], 'DateTime') ? $exception['recurrence_date'] :
+ rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
+ if (is_a($recurrence_id, 'DateTime')) {
+ $recurrence_id->add($date_shift);
+ $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
+ $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
+ }
+ }
+ }
+
+ // set link to top-level exceptions
+ $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+ }
+
+ // unset _dateonly flags in (cached) date objects
+ unset($event['start']->_dateonly, $event['end']->_dateonly);
+
+ $success = $storage->update_event($event) ? $event['id'] : false; // return master UID
+ break;
+ }
+
+ if ($success && $this->freebusy_trigger)
+ $this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
+
+ return $success;
+ }
+
+ /**
+ * Determine whether the current change affects scheduling and reset attendee status accordingly
+ */
+ public function check_scheduling(&$event, $old, $update = true)
+ {
+ // skip this check when importing iCal/iTip events
+ if (isset($event['sequence']) || !empty($event['_method'])) {
+ return false;
+ }
+
+ // iterate through the list of properties considered 'significant' for scheduling
+ $kolab_event = $old['_formatobj'] ?: new kolab_format_event();
+ $reschedule = $kolab_event->check_rescheduling($event, $old);
+
+ // reset all attendee status to needs-action (#4360)
+ if ($update && $reschedule && is_array($event['attendees'])) {
+ $is_organizer = false;
+ $emails = $this->cal->get_user_emails();
+ $attendees = $event['attendees'];
+ foreach ($attendees as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER' && $attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+ $is_organizer = true;
+ }
+ else if ($attendee['role'] != 'ORGANIZER' && $attendee['role'] != 'NON-PARTICIPANT' && $attendee['status'] != 'DELEGATED') {
+ $attendees[$i]['status'] = 'NEEDS-ACTION';
+ $attendees[$i]['rsvp'] = true;
+ }
+ }
+
+ // update attendees only if I'm the organizer
+ if ($is_organizer || ($event['organizer'] && in_array(strtolower($event['organizer']['email']), $emails))) {
+ $event['attendees'] = $attendees;
+ }
+ }
+
+ return $reschedule;
+ }
+
+ /**
+ * Apply the given changes to already existing exceptions
+ */
+ protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
+ {
+ $saved = false;
+ $existing = null;
+
+ // determine added and removed attendees
+ $added_attendees = $removed_attendees = array();
+ if ($savemode == 'future') {
+ $old_attendees = $current_attendees = array();
+ foreach ((array)$old['attendees'] as $attendee) {
+ $old_attendees[] = $attendee['email'];
+ }
+ foreach ((array)$event['attendees'] as $attendee) {
+ $current_attendees[] = $attendee['email'];
+ if (!in_array($attendee['email'], $old_attendees)) {
+ $added_attendees[] = $attendee;
+ }
+ }
+ $removed_attendees = array_diff($old_attendees, $current_attendees);
+ }
+
+ foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
+ // update a specific instance
+ if ($exception['_instance'] == $old['_instance']) {
+ $existing = $i;
+
+ // check savemode against existing exception mode.
+ // if matches, we can update this existing exception
+ if ((bool)$exception['thisandfuture'] === ($savemode == 'future')) {
+ $event['_instance'] = $old['_instance'];
+ $event['thisandfuture'] = $old['thisandfuture'];
+ $event['recurrence_date'] = $old['recurrence_date'];
+ $master['recurrence']['EXCEPTIONS'][$i] = $event;
+ $saved = true;
+ }
+ }
+ // merge the new event properties onto future exceptions
+ if ($savemode == 'future' && $exception['_instance'] >= $old['_instance']) {
+ unset($event['thisandfuture']);
+ self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, array('attendees'));
+
+ if (!empty($added_attendees) || !empty($removed_attendees)) {
+ calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
+ }
+ }
+ }
+/*
+ // we could not update the existing exception due to savemode mismatch...
+ if (!$saved && $existing !== null && $master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture']) {
+ // ... try to move the existing this-and-future exception to the next occurrence
+ foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
+ // our old this-and-future exception is obsolete
+ if ($candidate['thisandfuture']) {
+ unset($master['recurrence']['EXCEPTIONS'][$existing]);
+ $saved = true;
+ break;
+ }
+ // this occurrence doesn't yet have an exception
+ else if (!$candidate['isexception']) {
+ $event['_instance'] = $candidate['_instance'];
+ $event['recurrence_date'] = $candidate['recurrence_date'];
+ $master['recurrence']['EXCEPTIONS'][$i] = $event;
+ $saved = true;
+ break;
+ }
+ }
+ }
+*/
+
+ // set link to top-level exceptions
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+
+ // returning false here will add a new exception
+ return $saved;
+ }
+
+ /**
+ * Add or update the given event as an exception to $master
+ */
+ public static function add_exception(&$master, $event, $old = null)
+ {
+ if ($old) {
+ $event['_instance'] = $old['_instance'];
+ if (!$event['recurrence_date'])
+ $event['recurrence_date'] = $old['recurrence_date'] ?: $old['start'];
+ }
+ else if (!$event['recurrence_date']) {
+ $event['recurrence_date'] = $event['start'];
+ }
+
+ if (!$event['_instance'] && is_a($event['recurrence_date'], 'DateTime')) {
+ $event['_instance'] = libcalendaring::recurrence_instance_identifier($event);
+ }
+
+ if (!is_array($master['exceptions']) && is_array($master['recurrence']['EXCEPTIONS'])) {
+ $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
+ }
+
+ $existing = false;
+ foreach ((array)$master['exceptions'] as $i => $exception) {
+ if ($exception['_instance'] == $event['_instance']) {
+ $master['exceptions'][$i] = $event;
+ $existing = true;
+ }
+ }
+
+ if (!$existing) {
+ $master['exceptions'][] = $event;
+ }
+
+ return true;
+ }
+
+ /**
+ * Remove the noreply flags from attendees
+ */
+ public static function clear_attandee_noreply(&$event)
+ {
+ foreach ((array)$event['attendees'] as $i => $attendee) {
+ unset($event['attendees'][$i]['noreply']);
+ }
+ }
+
+
+ /**
+ * Merge certain properties from the overlay event to the base event object
+ *
+ * @param array The event object to be altered
+ * @param array The overlay event object to be merged over $event
+ * @param array List of properties not allowed to be overwritten
+ */
+ public static function merge_exception_data(&$event, $overlay, $blacklist = null)
+ {
+ $forbidden = array('id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments');
+
+ if (is_array($blacklist))
+ $forbidden = array_merge($forbidden, $blacklist);
+
+ // compute date offset from the exception
+ if ($overlay['start'] instanceof DateTime && $overlay['recurrence_date'] instanceof DateTime) {
+ $date_offset = $overlay['recurrence_date']->diff($overlay['start']);
+ }
+
+ foreach ($overlay as $prop => $value) {
+ if ($prop == 'start' || $prop == 'end') {
+ if (is_object($event[$prop]) && $event[$prop] instanceof DateTime) {
+ // set date value if overlay is an exception of the current instance
+ if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
+ $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
+ }
+ // apply date offset
+ else if ($date_offset) {
+ $event[$prop]->add($date_offset);
+ }
+ // adjust time of the recurring event instance
+ $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
+ }
+ }
+ else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
+ $event[$prop] = $value;
+ }
+ else if ($prop[0] != '_' && !in_array($prop, $forbidden))
+ $event[$prop] = $value;
+ }
+ }
+
+ /**
+ * Get events from source.
+ *
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
+ * @param boolean Include virtual events (optional)
+ * @param integer Only list events modified since this time (unix timestamp)
+ * @return array A list of event records
+ */
+ public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
+ {
+ if ($calendars && is_string($calendars))
+ $calendars = explode(',', $calendars);
+ else if (!$calendars)
+ $calendars = array_keys($this->calendars);
+
+ $query = array();
+ if ($modifiedsince)
+ $query[] = array('changed', '>=', $modifiedsince);
+
+ $events = $categories = array();
+ foreach ($calendars as $cid) {
+ if ($storage = $this->get_calendar($cid)) {
+ $events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
+ $categories += $storage->categories;
+ }
+ }
+
+ // add events from the address books birthday calendar
+ if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
+ $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
+ }
+
+ // add new categories to user prefs
+ $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
+ if ($newcats = array_udiff(array_keys($categories), array_keys($old_categories), function($a, $b){ return strcasecmp($a, $b); })) {
+ foreach ($newcats as $category)
+ $old_categories[$category] = ''; // no color set yet
+ $this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
+ }
+
+ array_walk($events, 'kolab_driver::to_rcube_event');
+ return $events;
+ }
+
+ /**
+ * Get number of events in the given calendar
+ *
+ * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return array Hash array with counts grouped by calendar ID
+ */
+ public function count_events($calendars, $start, $end = null)
+ {
+ $counts = array();
+
+ if ($calendars && is_string($calendars))
+ $calendars = explode(',', $calendars);
+ else if (!$calendars)
+ $calendars = array_keys($this->calendars);
+
+ foreach ($calendars as $cid) {
+ if ($storage = $this->get_calendar($cid)) {
+ $counts[$cid] = $storage->count_events($start, $end);
+ }
+ }
+
+ return $counts;
+ }
+
+ /**
+ * Get a list of pending alarms to be displayed to the user
+ *
+ * @see calendar_driver::pending_alarms()
+ */
+ public function pending_alarms($time, $calendars = null)
+ {
+ $interval = 300;
+ $time -= $time % 60;
+
+ $slot = $time;
+ $slot -= $slot % $interval;
+
+ $last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
+ $last -= $last % $interval;
+
+ // only check for alerts once in 5 minutes
+ if ($last == $slot)
+ return array();
+
+ if ($calendars && is_string($calendars))
+ $calendars = explode(',', $calendars);
+
+ $time = $slot + $interval;
+
+ $candidates = array();
+ $query = array(array('tags', '=', 'x-has-alarms'));
+ foreach ($this->calendars as $cid => $calendar) {
+ // skip calendars with alarms disabled
+ if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
+ continue;
+
+ foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
+ // add to list if alarm is set
+ $alarm = libcalendaring::get_next_alarm($e);
+ if ($alarm && $alarm['time'] && $alarm['time'] >= $last && in_array($alarm['action'], $this->alarm_types)) {
+ $id = $alarm['id']; // use alarm-id as primary identifier
+ $candidates[$id] = array(
+ 'id' => $id,
+ 'title' => $e['title'],
+ 'location' => $e['location'],
+ 'start' => $e['start'],
+ 'end' => $e['end'],
+ 'notifyat' => $alarm['time'],
+ 'action' => $alarm['action'],
+ );
+ }
+ }
+ }
+
+ // get alarm information stored in local database
+ if (!empty($candidates)) {
+ $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
+ $result = $this->rc->db->query("SELECT *"
+ . " FROM " . $this->rc->db->table_name('kolab_alarms', true)
+ . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
+ . " AND `user_id` = ?",
+ $this->rc->user->ID
+ );
+
+ while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
+ $dbdata[$e['alarm_id']] = $e;
+ }
+ }
+
+ $alarms = array();
+ foreach ($candidates as $id => $alarm) {
+ // skip dismissed alarms
+ if ($dbdata[$id]['dismissed'])
+ continue;
+
+ // snooze function may have shifted alarm time
+ $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
+ if ($notifyat <= $time)
+ $alarms[] = $alarm;
+ }
+
+ return $alarms;
+ }
+
+ /**
+ * Feedback after showing/sending an alarm notification
+ *
+ * @see calendar_driver::dismiss_alarm()
+ */
+ public function dismiss_alarm($alarm_id, $snooze = 0)
+ {
+ $alarms_table = $this->rc->db->table_name('kolab_alarms', true);
+ // delete old alarm entry
+ $this->rc->db->query("DELETE FROM $alarms_table"
+ . " WHERE `alarm_id` = ? AND `user_id` = ?",
+ $alarm_id,
+ $this->rc->user->ID
+ );
+
+ // set new notifyat time or unset if not snoozed
+ $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
+
+ $query = $this->rc->db->query("INSERT INTO $alarms_table"
+ . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
+ . " VALUES (?, ?, ?, ?)",
+ $alarm_id,
+ $this->rc->user->ID,
+ $snooze > 0 ? 0 : 1,
+ $notifyat
+ );
+
+ return $this->rc->db->affected_rows($query);
+ }
+
+ /**
+ * List attachments from the given event
+ */
+ public function list_attachments($event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar'])))
+ return false;
+
+ $event = $storage->get_event($event['id']);
+
+ return $event['attachments'];
+ }
+
+ /**
+ * Get attachment properties
+ */
+ public function get_attachment($id, $event)
+ {
+ if (!($storage = $this->get_calendar($event['calendar'])))
+ return false;
+
+ // get old revision of event
+ if ($event['rev']) {
+ $event = $this->get_event_revison($event, $event['rev'], true);
+ }
+ else {
+ $event = $storage->get_event($event['id']);
+ }
+
+ if ($event && !empty($event['_attachments'])) {
+ foreach ($event['_attachments'] as $att) {
+ if ($att['id'] == $id) {
+ return $att;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!($cal = $this->get_calendar($event['calendar'])))
+ return false;
+
+ // get old revision of event
+ if ($event['rev']) {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $cid = substr($id, 4);
+
+ // call Bonnie API and get the raw mime message
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+ if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['rev'], $mailbox, $msguid)) {
+ // parse the message and find the part with the matching content-id
+ $message = rcube_mime::parse_message($msg_raw);
+ foreach ((array)$message->parts as $part) {
+ if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
+ return $part->body;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ return $cal->get_attachment_body($id, $event);
+ }
+
+ /**
+ * Build a struct representing the given message reference
+ *
+ * @see calendar_driver::get_message_reference()
+ */
+ public function get_message_reference($uri_or_headers, $folder = null)
+ {
+ if (is_object($uri_or_headers)) {
+ $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
+ }
+
+ if (is_string($uri_or_headers)) {
+ return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
+ }
+
+ return false;
+ }
+
+ /**
+ * List availabale categories
+ * The default implementation reads them from config/user prefs
+ */
+ public function list_categories()
+ {
+ // FIXME: complete list with categories saved in config objects (KEP:12)
+ return $this->rc->config->get('calendar_categories', $this->default_categories);
+ }
+
+ /**
+ * Create instances of a recurring event
+ *
+ * @param array Hash array with event properties
+ * @param object DateTime Start date of the recurrence window
+ * @param object DateTime End date of the recurrence window
+ * @return array List of recurring event instances
+ */
+ public function get_recurring_events($event, $start, $end = null)
+ {
+ // load the given event data into a libkolabxml container
+ if (!$event['_formatobj']) {
+ $event_xml = new kolab_format_event();
+ $event_xml->set($event);
+ $event['_formatobj'] = $event_xml;
+ }
+
+ $this->_read_calendars();
+ $storage = reset($this->calendars);
+ return $storage->get_recurring_events($event, $start, $end);
+ }
+
+ /**
+ *
+ */
+ private function get_recurrence_count($event, $dtstart)
+ {
+ // use libkolab to compute recurring events
+ if (class_exists('kolabcalendaring') && $event['_formatobj']) {
+ $recurrence = new kolab_date_recurrence($event['_formatobj']);
+ }
+ else {
+ // fallback to local recurrence implementation
+ require_once($this->cal->home . '/lib/calendar_recurrence.php');
+ $recurrence = new calendar_recurrence($this->cal, $event);
+ }
+
+ $count = 0;
+ while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
+ $count++;
+ }
+
+ return $count;
+ }
+
+ /**
+ * Fetch free/busy information from a person within the given range
+ */
+ public function get_freebusy_list($email, $start, $end)
+ {
+ if (empty($email)/* || $end < time()*/)
+ return false;
+
+ // map vcalendar fbtypes to internal values
+ $fbtypemap = array(
+ 'FREE' => calendar::FREEBUSY_FREE,
+ 'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE,
+ 'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF,
+ 'OOF' => calendar::FREEBUSY_OOF);
+
+ // ask kolab server first
+ try {
+ $request_config = array(
+ 'store_body' => true,
+ 'follow_redirects' => true,
+ );
+ $request = libkolab::http_request(kolab_storage::get_freebusy_url($email), 'GET', $request_config);
+ $response = $request->send();
+
+ // authentication required
+ if ($response->getStatus() == 401) {
+ $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
+ $response = $request->send();
+ }
+
+ if ($response->getStatus() == 200)
+ $fbdata = $response->getBody();
+
+ unset($request, $response);
+ }
+ catch (Exception $e) {
+ PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage());
+ }
+
+ // get free-busy url from contacts
+ if (!$fbdata) {
+ $fburl = null;
+ foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
+ $abook = $this->rc->get_address_book($book);
+
+ if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) {
+ while ($contact = $result->iterate()) {
+ if ($fburl = $contact['freebusyurl']) {
+ $fbdata = @file_get_contents($fburl);
+ break;
+ }
+ }
+ }
+
+ if ($fbdata)
+ break;
+ }
+ }
+
+ // parse free-busy information using Horde classes
+ if ($fbdata) {
+ $ical = $this->cal->get_ical();
+ $ical->import($fbdata);
+ if ($fb = $ical->freebusy) {
+ $result = array();
+ foreach ($fb['periods'] as $tuple) {
+ list($from, $to, $type) = $tuple;
+ $result[] = array($from->format('U'), $to->format('U'), isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
+ }
+
+ // we take 'dummy' free-busy lists as "unknown"
+ if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy'))
+ return false;
+
+ // set period from $start till the begin of the free-busy information as 'unknown'
+ if ($fb['start'] && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
+ array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
+ }
+ // pad period till $end with status 'unknown'
+ if ($fb['end'] && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
+ $result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
+ }
+
+ return $result;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Handler to push folder triggers when sent from client.
+ * Used to push free-busy changes asynchronously after updating an event
+ */
+ public function push_freebusy()
+ {
+ // make shure triggering completes
+ set_time_limit(0);
+ ignore_user_abort(true);
+
+ $cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
+ if (!($cal = $this->get_calendar($cal)))
+ return false;
+
+ // trigger updates on folder
+ $trigger = $cal->storage->trigger();
+ if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
+ rcube::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
+ true, false);
+ }
+
+ exit;
+ }
+
+
+ /**
+ * Convert from driver format to external caledar app data
+ */
+ public static function to_rcube_event(&$record)
+ {
+ if (!is_array($record))
+ return $record;
+
+ $record['id'] = $record['uid'];
+
+ if ($record['_instance']) {
+ $record['id'] .= '-' . $record['_instance'];
+
+ if (!$record['recurrence_id'] && !empty($record['recurrence']))
+ $record['recurrence_id'] = $record['uid'];
+ }
+
+ // all-day events go from 12:00 - 13:00
+ if (is_a($record['start'], 'DateTime') && $record['end'] <= $record['start'] && $record['allday']) {
+ $record['end'] = clone $record['start'];
+ $record['end']->add(new DateInterval('PT1H'));
+ }
+
+ // translate internal '_attachments' to external 'attachments' list
+ if (!empty($record['_attachments'])) {
+ foreach ($record['_attachments'] as $key => $attachment) {
+ if ($attachment !== false) {
+ if (!$attachment['name'])
+ $attachment['name'] = $key;
+
+ unset($attachment['path'], $attachment['content']);
+ $attachments[] = $attachment;
+ }
+ }
+
+ $record['attachments'] = $attachments;
+ }
+
+ if (!empty($record['attendees'])) {
+ foreach ((array)$record['attendees'] as $i => $attendee) {
+ if (is_array($attendee['delegated-from'])) {
+ $record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
+ }
+ if (is_array($attendee['delegated-to'])) {
+ $record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
+ }
+ }
+ }
+
+ // Roundcube only supports one category assignment
+ if (is_array($record['categories']))
+ $record['categories'] = $record['categories'][0];
+
+ // the cancelled flag transltes into status=CANCELLED
+ if ($record['cancelled'])
+ $record['status'] = 'CANCELLED';
+
+ // The web client only supports DISPLAY type of alarms
+ if (!empty($record['alarms']))
+ $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
+
+ // remove empty recurrence array
+ if (empty($record['recurrence']))
+ unset($record['recurrence']);
+
+ // clean up exception data
+ if (is_array($record['recurrence']['EXCEPTIONS'])) {
+ array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
+ unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
+ });
+ }
+
+ unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
+ $record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']);
+
+ return $record;
+ }
+
+ /**
+ *
+ */
+ public static function from_rcube_event($event, $old = array())
+ {
+ // in kolab_storage attachments are indexed by content-id
+ if (is_array($event['attachments']) || !empty($event['deleted_attachments'])) {
+ $event['_attachments'] = array();
+
+ foreach ($event['attachments'] as $attachment) {
+ $key = null;
+ // Roundcube ID has nothing to do with the storage ID, remove it
+ if ($attachment['content'] || $attachment['path']) {
+ unset($attachment['id']);
+ }
+ else {
+ foreach ((array)$old['_attachments'] as $cid => $oldatt) {
+ if ($attachment['id'] == $oldatt['id'])
+ $key = $cid;
+ }
+ }
+
+ // flagged for deletion => set to false
+ if ($attachment['_deleted'] || in_array($attachment['id'], (array)$event['deleted_attachments'])) {
+ $event['_attachments'][$key] = false;
+ }
+ // replace existing entry
+ else if ($key) {
+ $event['_attachments'][$key] = $attachment;
+ }
+ // append as new attachment
+ else {
+ $event['_attachments'][] = $attachment;
+ }
+ }
+
+ $event['_attachments'] = array_merge((array)$old['_attachments'], $event['_attachments']);
+
+ // attachments flagged for deletion => set to false
+ foreach ($event['_attachments'] as $key => $attachment) {
+ if ($attachment['_deleted'] || in_array($attachment['id'], (array)$event['deleted_attachments'])) {
+ $event['_attachments'][$key] = false;
+ }
+ }
+ }
+
+ return $event;
+ }
+
+
+ /**
+ * Set CSS class according to the event's attendde partstat
+ */
+ public static function add_partstat_class($event, $partstats, $user = null)
+ {
+ // set classes according to PARTSTAT
+ if (is_array($event['attendees'])) {
+ $user_emails = libcalendaring::get_instance()->get_user_emails($user);
+ $partstat = 'UNKNOWN';
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails)) {
+ $partstat = $attendee['status'];
+ break;
+ }
+ }
+
+ if (in_array($partstat, $partstats)) {
+ $event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
+ }
+ }
+
+ return $event;
+ }
+
+ /**
+ * Provide a list of revisions for the given event
+ *
+ * @param array $event Hash array with event properties
+ *
+ * @return array List of changes, each as a hash array
+ * @see calendar_driver::get_event_changelog()
+ */
+ public function get_event_changelog($event)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ $result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid);
+ if (is_array($result) && $result['uid'] == $uid) {
+ return $result['changes'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get a list of property changes beteen two revisions of an event
+ *
+ * @param array $event Hash array with event properties
+ * @param mixed $rev1 Old Revision
+ * @param mixed $rev2 New Revision
+ *
+ * @return array List of property changes, each as a hash array
+ * @see calendar_driver::get_event_diff()
+ */
+ public function get_event_diff($event, $rev1, $rev2)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ // get diff for the requested recurrence instance
+ $instance_id = $event['id'] != $uid ? substr($event['id'], strlen($uid) + 1) : null;
+
+ // call Bonnie API
+ $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
+ if (is_array($result) && $result['uid'] == $uid) {
+ $result['rev1'] = $rev1;
+ $result['rev2'] = $rev2;
+
+ $keymap = array(
+ 'dtstart' => 'start',
+ 'dtend' => 'end',
+ 'dstamp' => 'changed',
+ 'summary' => 'title',
+ 'alarm' => 'alarms',
+ 'attendee' => 'attendees',
+ 'attach' => 'attachments',
+ 'rrule' => 'recurrence',
+ 'transparency' => 'free_busy',
+ 'classification' => 'sensitivity',
+ 'lastmodified-date' => 'changed',
+ );
+ $prop_keymaps = array(
+ 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
+ 'attendees' => array('partstat' => 'status'),
+ );
+ $special_changes = array();
+
+ // map kolab event properties to keys the client expects
+ array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
+ if (array_key_exists($change['property'], $keymap)) {
+ $change['property'] = $keymap[$change['property']];
+ }
+ // translate free_busy values
+ if ($change['property'] == 'free_busy') {
+ $change['old'] = $old['old'] ? 'free' : 'busy';
+ $change['new'] = $old['new'] ? 'free' : 'busy';
+ }
+ // map alarms trigger value
+ if ($change['property'] == 'alarms') {
+ if (is_array($change['old']) && is_array($change['old']['trigger']))
+ $change['old']['trigger'] = $change['old']['trigger']['value'];
+ if (is_array($change['new']) && is_array($change['new']['trigger']))
+ $change['new']['trigger'] = $change['new']['trigger']['value'];
+ }
+ // make all property keys uppercase
+ if ($change['property'] == 'recurrence') {
+ $special_changes['recurrence'] = $i;
+ foreach (array('old','new') as $m) {
+ if (is_array($change[$m])) {
+ $props = array();
+ foreach ($change[$m] as $k => $v)
+ $props[strtoupper($k)] = $v;
+ $change[$m] = $props;
+ }
+ }
+ }
+ // map property keys names
+ if (is_array($prop_keymaps[$change['property']])) {
+ foreach ($prop_keymaps[$change['property']] as $k => $dest) {
+ if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
+ $change['old'][$dest] = $change['old'][$k];
+ unset($change['old'][$k]);
+ }
+ if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
+ $change['new'][$dest] = $change['new'][$k];
+ unset($change['new'][$k]);
+ }
+ }
+ }
+
+ if ($change['property'] == 'exdate') {
+ $special_changes['exdate'] = $i;
+ }
+ else if ($change['property'] == 'rdate') {
+ $special_changes['rdate'] = $i;
+ }
+ });
+
+ // merge some recurrence changes
+ foreach (array('exdate','rdate') as $prop) {
+ if (array_key_exists($prop, $special_changes)) {
+ $exdate = $result['changes'][$special_changes[$prop]];
+ if (array_key_exists('recurrence', $special_changes)) {
+ $recurrence = &$result['changes'][$special_changes['recurrence']];
+ }
+ else {
+ $i = count($result['changes']);
+ $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
+ $recurrence = &$result['changes'][$i]['recurrence'];
+ }
+ $key = strtoupper($prop);
+ $recurrence['old'][$key] = $exdate['old'];
+ $recurrence['new'][$key] = $exdate['new'];
+ unset($result['changes'][$special_changes[$prop]]);
+ }
+ }
+
+ return $result;
+ }
+
+ return false;
+ }
+
+ /**
+ * Return full data of a specific revision of an event
+ *
+ * @param array Hash array with event properties
+ * @param mixed $rev Revision number
+ *
+ * @return array Event object as hash array
+ * @see calendar_driver::get_event_revison()
+ */
+ public function get_event_revison($event, $rev, $internal = false)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ $eventid = $event['id'];
+ $calid = $event['calendar'];
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+
+ // call Bonnie API
+ $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid);
+ if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
+ $format = kolab_format::factory('event');
+ $format->load($result['xml']);
+ $event = $format->to_array();
+ $format->get_attachments($event, true);
+
+ // get the right instance from a recurring event
+ if ($eventid != $event['uid']) {
+ $instance_id = substr($eventid, strlen($event['uid']) + 1);
+
+ // check for recurrence exception first
+ if ($instance = $format->get_instance($instance_id)) {
+ $event = $instance;
+ }
+ else {
+ // not a exception, compute recurrence...
+ $event['_formatobj'] = $format;
+ $recurrence_date = rcube_utils::anytodatetime($instance_id, $event['start']->getTimezone());
+ foreach ($this->get_recurring_events($event, $event['start'], $recurrence_date) as $instance) {
+ if ($instance['id'] == $eventid) {
+ $event = $instance;
+ break;
+ }
+ }
+ }
+ }
+
+ if ($format->is_valid()) {
+ $event['calendar'] = $calid;
+ $event['rev'] = $result['rev'];
+ return $internal ? $event : self::to_rcube_event($event);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Command the backend to restore a certain revision of an event.
+ * This shall replace the current event with an older version.
+ *
+ * @param mixed UID string or hash array with event properties:
+ * id: Event identifier
+ * calendar: Calendar identifier
+ * @param mixed $rev Revision number
+ *
+ * @return boolean True on success, False on failure
+ */
+ public function restore_event_revision($event, $rev)
+ {
+ if (empty($this->bonnie_api)) {
+ return false;
+ }
+
+ list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
+ $calendar = $this->get_calendar($event['calendar']);
+ $success = false;
+
+ if ($calendar && $calendar->storage && $calendar->editable) {
+ if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) {
+ $imap = $this->rc->get_storage();
+
+ // insert $raw_msg as new message
+ if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) {
+ $success = true;
+
+ // delete old revision from imap and cache
+ $imap->delete_message($msguid, $calendar->storage->name);
+ $calendar->storage->cache->set($msguid, false);
+ }
+ }
+ }
+
+ return $success;
+ }
+
+ /**
+ * Helper method to resolved the given event identifier into uid and folder
+ *
+ * @return array (uid,folder,msguid) tuple
+ */
+ private function _resolve_event_identity($event)
+ {
+ $mailbox = $msguid = null;
+ if (is_array($event)) {
+ $uid = $event['uid'] ?: $event['id'];
+ if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
+ $mailbox = $cal->get_mailbox_id();
+
+ // get event object from storage in order to get the real object uid an msguid
+ if ($ev = $cal->get_event($event['id'])) {
+ $msguid = $ev['_msguid'];
+ $uid = $ev['uid'];
+ }
+ }
+ }
+ else {
+ $uid = $event;
+
+ // get event object from storage in order to get the real object uid an msguid
+ if ($ev = $this->get_event($event)) {
+ $mailbox = $ev['_mailbox'];
+ $msguid = $ev['_msguid'];
+ $uid = $ev['uid'];
+ }
+ }
+
+ return array($uid, $mailbox, $msguid);
+ }
+
+ /**
+ * Callback function to produce driver-specific calendar create/edit form
+ *
+ * @param string Request action 'form-edit|form-new'
+ * @param array Calendar properties (e.g. id, color)
+ * @param array Edit form fields
+ *
+ * @return string HTML content of the form
+ */
+ public function calendar_form($action, $calendar, $formfields)
+ {
+ // show default dialog for birthday calendar
+ if (in_array($calendar['id'], array(self::BIRTHDAY_CALENDAR_ID, self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED))) {
+ if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID)
+ unset($formfields['showalarms']);
+ return parent::calendar_form($action, $calendar, $formfields);
+ }
+
+ if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
+ $folder = $cal->get_realname(); // UTF7
+ $color = $cal->get_color();
+ }
+ else {
+ $folder = '';
+ $color = '';
+ }
+
+ $hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
+
+ $storage = $this->rc->get_storage();
+ $delim = $storage->get_hierarchy_delimiter();
+ $form = array();
+
+ if (strlen($folder)) {
+ $path_imap = explode($delim, $folder);
+ array_pop($path_imap); // pop off name part
+ $path_imap = implode($path_imap, $delim);
+
+ $options = $storage->folder_info($folder);
+ }
+ else {
+ $path_imap = '';
+ }
+
+ // General tab
+ $form['props'] = array(
+ 'name' => $this->rc->gettext('properties'),
+ );
+
+ // Disable folder name input
+ if (!empty($options) && ($options['norename'] || $options['protected'])) {
+ $input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
+ $formfields['name']['value'] = kolab_storage::object_name($folder)
+ . $input_name->show($folder);
+ }
+
+ // calendar name (default field)
+ $form['props']['fieldsets']['location'] = array(
+ 'name' => $this->rc->gettext('location'),
+ 'content' => array(
+ 'name' => $formfields['name']
+ ),
+ );
+
+ if (!empty($options) && ($options['norename'] || $options['protected'])) {
+ // prevent user from moving folder
+ $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
+ }
+ else {
+ $select = kolab_storage::folder_selector('event', array('name' => 'parent', 'id' => 'calendar-parent'), $folder);
+ $form['props']['fieldsets']['location']['content']['path'] = array(
+ 'id' => 'calendar-parent',
+ 'label' => $this->cal->gettext('parentcalendar'),
+ 'value' => $select->show(strlen($folder) ? $path_imap : ''),
+ );
+ }
+
+ // calendar color (default field)
+ $form['props']['fieldsets']['settings'] = array(
+ 'name' => $this->rc->gettext('settings'),
+ 'content' => array(
+ 'color' => $formfields['color'],
+ 'showalarms' => $formfields['showalarms'],
+ ),
+ );
+
+
+ if ($action != 'form-new') {
+ $form['sharing'] = array(
+ 'name' => Q($this->cal->gettext('tabsharing')),
+ 'content' => html::tag('iframe', array(
+ 'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)),
+ 'width' => '100%',
+ 'height' => 350,
+ 'border' => 0,
+ 'style' => 'border:0'),
+ ''),
+ );
+ }
+
+ $this->form_html = '';
+ if (is_array($hidden_fields)) {
+ foreach ($hidden_fields as $field) {
+ $hiddenfield = new html_hiddenfield($field);
+ $this->form_html .= $hiddenfield->show() . "\n";
+ }
+ }
+
+ // Create form output
+ foreach ($form as $tab) {
+ if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
+ $content = '';
+ foreach ($tab['fieldsets'] as $fieldset) {
+ $subcontent = $this->get_form_part($fieldset);
+ if ($subcontent) {
+ $content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
+ }
+ }
+ }
+ else {
+ $content = $this->get_form_part($tab);
+ }
+
+ if ($content) {
+ $this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
+ }
+ }
+
+ // Parse form template for skin-dependent stuff
+ $this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html'));
+ return $this->rc->output->parse('calendar.kolabform', false, false);
+ }
+
+ /**
+ * Handler for template object
+ */
+ public function calendar_form_html()
+ {
+ return $this->form_html;
+ }
+
+ /**
+ * Helper function used in calendar_form_content(). Creates a part of the form.
+ */
+ private function get_form_part($form)
+ {
+ $content = '';
+
+ if (is_array($form['content']) && !empty($form['content'])) {
+ $table = new html_table(array('cols' => 2));
+ foreach ($form['content'] as $col => $colprop) {
+ $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
+
+ $table->add('title', html::label($colprop['id'], Q($label)));
+ $table->add(null, $colprop['value']);
+ }
+ $content = $table->show();
+ }
+ else {
+ $content = $form['content'];
+ }
+
+ return $content;
+ }
+
+
+ /**
+ * Handler to render ACL form for a calendar folder
+ */
+ public function calendar_acl()
+ {
+ $this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form'));
+ $this->rc->output->send('calendar.kolabacl');
+ }
+
+ /**
+ * Handler for ACL form template object
+ */
+ public function calendar_acl_form()
+ {
+ $calid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+ if ($calid && ($cal = $this->get_calendar($calid))) {
+ $folder = $cal->get_realname(); // UTF7
+ $color = $cal->get_color();
+ }
+ else {
+ $folder = '';
+ $color = '';
+ }
+
+ $storage = $this->rc->get_storage();
+ $delim = $storage->get_hierarchy_delimiter();
+ $form = array();
+
+ if (strlen($folder)) {
+ $path_imap = explode($delim, $folder);
+ array_pop($path_imap); // pop off name part
+ $path_imap = implode($path_imap, $delim);
+
+ $options = $storage->folder_info($folder);
+
+ // Allow plugins to modify the form content (e.g. with ACL form)
+ $plugin = $this->rc->plugins->exec_hook('calendar_form_kolab',
+ array('form' => $form, 'options' => $options, 'name' => $folder));
+ }
+
+ if (!$plugin['form']['sharing']['content'])
+ $plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights'));
+
+ return $plugin['form']['sharing']['content'];
+ }
+
+ /**
+ * Handler for user_delete plugin hook
+ */
+ public function user_delete($args)
+ {
+ $db = $this->rc->get_dbh();
+ foreach (array('kolab_alarms', 'itipinvitations') as $table) {
+ $db->query("DELETE FROM " . $this->rc->db->table_name($table, true)
+ . " WHERE `user_id` = ?", $args['user']->ID);
+ }
+ }
+}
diff --git a/calendar/drivers/kolab/kolab_invitation_calendar.php b/calendar/drivers/kolab/kolab_invitation_calendar.php
new file mode 100644
index 0000000..3ec82ac
--- /dev/null
+++ b/calendar/drivers/kolab/kolab_invitation_calendar.php
@@ -0,0 +1,377 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual calendar listing pedning/declined invitations
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_invitation_calendar
+{
+ public $id = '__invitation__';
+ public $ready = true;
+ public $alarms = false;
+ public $rights = 'lrsv';
+ public $editable = false;
+ public $attachments = false;
+ public $subscriptions = false;
+ public $partstats = array('unknown');
+ public $categories = array();
+ public $name = 'Invitations';
+
+ /**
+ * Default constructor
+ */
+ public function __construct($id, $calendar)
+ {
+ $this->cal = $calendar;
+ $this->id = $id;
+
+ switch ($this->id) {
+ case kolab_driver::INVITATIONS_CALENDAR_PENDING:
+ $this->partstats = array('NEEDS-ACTION');
+ $this->name = $this->cal->gettext('invitationspending');
+ if (!empty($_REQUEST['_quickview']))
+ $this->partstats[] = 'TENTATIVE';
+ break;
+
+ case kolab_driver::INVITATIONS_CALENDAR_DECLINED:
+ $this->partstats = array('DECLINED');
+ $this->name = $this->cal->gettext('invitationsdeclined');
+ break;
+ }
+
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+ if (isset($prefs[$this->id]['showalarms']))
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ }
+
+
+ /**
+ * Getter for a nice and human readable name for this calendar
+ *
+ * @return string Name of this calendar
+ */
+ public function get_name()
+ {
+ return $this->name;
+ }
+
+
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner()
+ {
+ return $this->cal->rc->get_user_name();
+ }
+
+
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return $this->get_name();
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return 'x-special';
+ }
+
+
+ /**
+ * Getter for the top-end calendar folder name (not the entire path)
+ *
+ * @return string Name of this calendar
+ */
+ public function get_foldername()
+ {
+ return $this->get_name();
+ }
+
+ /**
+ * Getter for the Cyrus mailbox identifier corresponding to this folder
+ *
+ * @return string Mailbox ID
+ */
+ public function get_mailbox_id()
+ {
+ // this is a virtual collection and has no concrete mailbox ID
+ return null;
+ }
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color()
+ {
+ // calendar color is stored in local user prefs
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+ return $prefs[$this->id]['color'];
+
+ return 'ffffff';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ return false;
+ }
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return boolean True if enabled, false if not
+ */
+ public function is_active()
+ {
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array()); // read local prefs
+ return (bool)$prefs[$this->id]['active'];
+ }
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ // don't change anything.
+ // let kolab_driver save props in local prefs
+ return $prop['id'];
+ }
+
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // redirect call to kolab_driver::get_event()
+ $event = $this->cal->driver->get_event($id, calendar_driver::FILTER_WRITEABLE);
+
+ if (is_array($event)) {
+ // add pointer to original calendar folder
+ $event['_folder_id'] = $event['calendar'];
+ $event = $this->_mod_event($event);
+ }
+
+ return $event;
+ }
+
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ // find the actual folder this event resides in
+ if (!empty($event['_folder_id'])) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ }
+ else {
+ $cal = null;
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ if ($cal->ready && $cal->storage && $cal->get_event($event['id'])) {
+ break;
+ }
+ }
+ }
+
+ if ($cal && $cal->storage) {
+ return $cal->get_attachment_body($id, $event);
+ }
+
+ return false;
+ }
+
+
+ /**
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param boolean Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+ {
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+ $subquery = array();
+ foreach ($user_emails as $email) {
+ foreach ($this->partstats as $partstat) {
+ $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat));
+ }
+ }
+
+ // aggregate events from all calendar folders
+ $events = array();
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ if ($cal->get_namespace() == 'other')
+ continue;
+
+ foreach ($cal->list_events($start, $end, $search, 1, $query, array(array($subquery, 'OR'))) as $event) {
+ $match = false;
+
+ // post-filter events to match out partstats
+ if (is_array($event['attendees'])) {
+ foreach ($event['attendees'] as $attendee) {
+ if (in_array($attendee['email'], $user_emails) && in_array($attendee['status'], $this->partstats)) {
+ $match = true;
+ break;
+ }
+ }
+ }
+
+ if ($match) {
+ $events[$event['id']] = $this->_mod_event($event);
+ }
+ }
+
+ // merge list of event categories (really?)
+ $this->categories += $cal->categories;
+ }
+
+ return $events;
+ }
+
+ /**
+ *
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return integer Count
+ */
+ public function count_events($start, $end = null)
+ {
+ // get email addresses of the current user
+ $user_emails = $this->cal->get_user_emails();
+ $subquery = array();
+ foreach ($user_emails as $email) {
+ foreach ($this->partstats as $partstat) {
+ $subquery[] = array('tags', '=', 'x-partstat:' . $email . ':' . strtolower($partstat));
+ }
+ }
+
+ $filter = array(
+ array('tags','!=','x-status:cancelled'),
+ array($subquery, 'OR')
+ );
+
+ // aggregate counts from all calendar folders
+ $count = 0;
+ foreach (kolab_storage::list_folders('', '*', 'event', null) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ if ($cal->get_namespace() == 'other')
+ continue;
+
+ $count += $cal->count_events($start, $end, $filter);
+ }
+
+ return $count;
+ }
+
+ /**
+ * Helper method to modify some event properties
+ */
+ private function _mod_event($event)
+ {
+ // set classes according to PARTSTAT
+ $event = kolab_driver::add_partstat_class($event, $this->partstats);
+
+ if (strpos($event['className'], 'fc-invitation-') !== false) {
+ $event['calendar'] = $this->id;
+ }
+
+ return $event;
+ }
+
+
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ * @return boolean True on success, False on error
+ */
+
+ public function update_event($event, $exception_id = null)
+ {
+ // forward call to the actual storage folder
+ if ($event['_folder_id']) {
+ $cal = $this->cal->driver->get_calendar($event['_folder_id']);
+ if ($cal && $cal->ready) {
+ return $cal->update_event($event, $exception_id);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ * @return boolean True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ return false;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
+
+
+}
diff --git a/calendar/drivers/kolab/kolab_user_calendar.php b/calendar/drivers/kolab/kolab_user_calendar.php
new file mode 100644
index 0000000..00f1dfc
--- /dev/null
+++ b/calendar/drivers/kolab/kolab_user_calendar.php
@@ -0,0 +1,432 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual user calendar
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_user_calendar extends kolab_calendar
+{
+ public $id = 'unknown';
+ public $ready = false;
+ public $editable = false;
+ public $attachments = false;
+ public $subscriptions = false;
+
+ protected $userdata = array();
+ protected $timeindex = array();
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($user_or_folder, $calendar)
+ {
+ $this->cal = $calendar;
+
+ // full user record is provided
+ if (is_array($user_or_folder)) {
+ $this->userdata = $user_or_folder;
+ $this->storage = new kolab_storage_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata);
+ }
+ else { // get user record from LDAP
+ $this->storage = new kolab_storage_folder_user($user_or_folder);
+ $this->userdata = $this->storage->ldaprec;
+ }
+
+ $this->ready = !empty($this->userdata['kolabtargetfolder']);
+ $this->storage->type = 'event';
+
+ if ($this->ready) {
+ // ID is derrived from the user's kolabtargetfolder attribute
+ $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
+ $this->imap_folder = $this->userdata['kolabtargetfolder'];
+ $this->name = $this->storage->get_name();
+ $this->parent = ''; // user calendars are top level
+
+ // user-specific alarms settings win
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+ if (isset($prefs[$this->id]['showalarms']))
+ $this->alarms = $prefs[$this->id]['showalarms'];
+ }
+ }
+
+
+ /**
+ * Getter for a nice and human readable name for this calendar
+ *
+ * @return string Name of this calendar
+ */
+ public function get_name()
+ {
+ return $this->userdata['displayname'] ?: ($this->userdata['name'] ?: $this->userdata['mail']);
+ }
+
+
+ /**
+ * Getter for the IMAP folder owner
+ *
+ * @return string Name of the folder owner
+ */
+ public function get_owner()
+ {
+ return $this->userdata['mail'];
+ }
+
+
+ /**
+ *
+ */
+ public function get_title()
+ {
+ return trim($this->userdata['displayname'] . '; ' . $this->userdata['mail'], '; ');
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ return 'other user';
+ }
+
+
+ /**
+ * Getter for the top-end calendar folder name (not the entire path)
+ *
+ * @return string Name of this calendar
+ */
+ public function get_foldername()
+ {
+ return $this->get_name();
+ }
+
+ /**
+ * Return color to display this calendar
+ */
+ public function get_color()
+ {
+ // calendar color is stored in local user prefs
+ $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+ if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+ return $prefs[$this->id]['color'];
+
+ return 'cc0000';
+ }
+
+ /**
+ * Compose an URL for CalDAV access to this calendar (if configured)
+ */
+ public function get_caldav_url()
+ {
+ return false;
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return $this->storage->is_subscribed();
+ }
+
+ /**
+ * Update properties of this calendar folder
+ *
+ * @see calendar_driver::edit_calendar()
+ */
+ public function update(&$prop)
+ {
+ // don't change anything.
+ // let kolab_driver save props in local prefs
+ return $prop['id'];
+ }
+
+
+ /**
+ * Getter for a single event object
+ */
+ public function get_event($id)
+ {
+ // TODO: implement this
+ return $this->events[$id];
+ }
+
+ /**
+ * Get attachment body
+ * @see calendar_driver::get_attachment_body()
+ */
+ public function get_attachment_body($id, $event)
+ {
+ if (!$event['calendar'] && ($ev = $this->get_event($event['id']))) {
+ $event['calendar'] = $ev['calendar'];
+ }
+
+ if ($event['calendar'] && ($cal = $this->cal->get_calendar($event['calendar']))) {
+ return $cal->get_attachment_body($id, $event);
+ }
+
+ return false;
+ }
+
+ /**
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @param string Search query (optional)
+ * @param boolean Include virtual events (optional)
+ * @param array Additional parameters to query storage
+ * @return array A list of event records
+ */
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+ {
+ // convert to DateTime for comparisons
+ try {
+ $start_dt = new DateTime('@'.$start);
+ }
+ catch (Exception $e) {
+ $start_dt = new DateTime('@0');
+ }
+ try {
+ $end_dt = new DateTime('@'.$end);
+ }
+ catch (Exception $e) {
+ $end_dt = new DateTime('today +10 years');
+ }
+
+ $limit_changed = null;
+ if (!empty($query)) {
+ foreach ($query as $q) {
+ if ($q[0] == 'changed' && $q[1] == '>=') {
+ try { $limit_changed = new DateTime('@'.$q[2]); }
+ catch (Exception $e) { /* ignore */ }
+ }
+ }
+ }
+
+ // aggregate all calendar folders the user shares (but are not subscribed)
+ foreach (kolab_storage::list_user_folders($this->userdata, 'event', false) as $foldername) {
+ $cal = new kolab_calendar($foldername, $this->cal);
+ foreach ($cal->list_events($start, $end, $search, 1) as $event) {
+ $this->events[$event['id']] = $event;
+ $this->timeindex[$this->time_key($event)] = $event['id'];
+ }
+ }
+
+ // get events from the user's free/busy feed (for quickview only)
+ $fbview = $this->cal->rc->config->get('calendar_include_freebusy_data', 1);
+ if ($fbview && ($fbview == 1 || !empty($_REQUEST['_quickview'])) && empty($search)) {
+ $this->fetch_freebusy($limit_changed);
+ }
+
+ $events = array();
+ foreach ($this->events as $event) {
+ // list events in requested time window
+ if ($event['start'] <= $end_dt && $event['end'] >= $start_dt &&
+ (!$limit_changed || !$event['changed'] || $event['changed'] >= $limit_changed)) {
+ $events[] = $event;
+ }
+ }
+
+ // avoid session race conditions that will loose temporary subscriptions
+ $this->cal->rc->session->nowrite = true;
+
+ return $events;
+ }
+
+ /**
+ *
+ * @param integer Date range start (unix timestamp)
+ * @param integer Date range end (unix timestamp)
+ * @return integer Count
+ */
+ public function count_events($start, $end = null)
+ {
+ // not implemented
+ return 0;
+ }
+
+ /**
+ * Helper method to fetch free/busy data for the user and turn it into calendar data
+ */
+ private function fetch_freebusy($limit_changed = null)
+ {
+ // ask kolab server first
+ try {
+ $request_config = array(
+ 'store_body' => true,
+ 'follow_redirects' => true,
+ );
+ $request = libkolab::http_request(kolab_storage::get_freebusy_url($this->userdata['mail']), 'GET', $request_config);
+ $response = $request->send();
+
+ // authentication required
+ if ($response->getStatus() == 401) {
+ $request->setAuth($this->cal->rc->user->get_username(), $this->cal->rc->decrypt($_SESSION['password']));
+ $response = $request->send();
+ }
+
+ if ($response->getStatus() == 200)
+ $fbdata = $response->getBody();
+
+ unset($request, $response);
+ }
+ catch (Exception $e) {
+ rcube::raise_error(array(
+ 'code' => 900,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Error fetching free/busy information: " . $e->getMessage()),
+ true, false);
+
+ return false;
+ }
+
+ $statusmap = array(
+ 'FREE' => 'free',
+ 'BUSY' => 'busy',
+ 'BUSY-TENTATIVE' => 'tentative',
+ 'X-OUT-OF-OFFICE' => 'outofoffice',
+ 'OOF' => 'outofoffice',
+ );
+ $titlemap = array(
+ 'FREE' => $this->cal->gettext('availfree'),
+ 'BUSY' => $this->cal->gettext('availbusy'),
+ 'BUSY-TENTATIVE' => $this->cal->gettext('availtentative'),
+ 'X-OUT-OF-OFFICE' => $this->cal->gettext('availoutofoffice'),
+ );
+
+ // console('_fetch_freebusy', kolab_storage::get_freebusy_url($this->userdata['mail']), $fbdata);
+
+ // parse free-busy information
+ $count = 0;
+ if ($fbdata) {
+ $ical = $this->cal->get_ical();
+ $ical->import($fbdata);
+ if ($fb = $ical->freebusy) {
+ // consider 'changed >= X' queries
+ if ($limit_changed && $fb['created'] && $fb['created'] < $limit_changed) {
+ return 0;
+ }
+
+ foreach ($fb['periods'] as $tuple) {
+ list($from, $to, $type) = $tuple;
+ $event = array(
+ 'id' => md5($this->id . $from->format('U') . '/' . $to->format('U')),
+ 'calendar' => $this->id,
+ 'changed' => $fb['created'] ?: new DateTime(),
+ 'title' => $this->get_name() . ' ' . ($titlemap[$type] ?: $type),
+ 'start' => $from,
+ 'end' => $to,
+ 'free_busy' => $statusmap[$type] ?: 'busy',
+ 'className' => 'fc-type-freebusy',
+ 'organizer' => array(
+ 'email' => $this->userdata['mail'],
+ 'name' => $this->userdata['displayname'],
+ ),
+ );
+
+ // avoid duplicate entries
+ $key = $this->time_key($event);
+ if (!$this->timeindex[$key]) {
+ $this->events[$event['id']] = $event;
+ $this->timeindex[$key] = $event['id'];
+ $count++;
+ }
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Helper to build a key for the absolute time slot the given event convers
+ */
+ private function time_key($event)
+ {
+ return sprintf('%s/%s', $event['start']->format('U'), is_object($event['end']->format('U')) ?: '0');
+ }
+
+
+ /**
+ * Create a new event record
+ *
+ * @see calendar_driver::new_event()
+ *
+ * @return mixed The created record ID on success, False on error
+ */
+ public function insert_event($event)
+ {
+ return false;
+ }
+
+ /**
+ * Update a specific event record
+ *
+ * @see calendar_driver::new_event()
+ * @return boolean True on success, False on error
+ */
+
+ public function update_event($event, $exception_id = null)
+ {
+ return false;
+ }
+
+ /**
+ * Delete an event record
+ *
+ * @see calendar_driver::remove_event()
+ * @return boolean True on success, False on error
+ */
+ public function delete_event($event, $force = true)
+ {
+ return false;
+ }
+
+ /**
+ * Restore deleted event record
+ *
+ * @see calendar_driver::undelete_event()
+ * @return boolean True on success, False on error
+ */
+ public function restore_event($event)
+ {
+ return false;
+ }
+
+
+ /**
+ * Convert from Kolab_Format to internal representation
+ */
+ private function _to_rcube_event($record)
+ {
+ $record['id'] = $record['uid'];
+ $record['calendar'] = $this->id;
+
+ return kolab_driver::to_rcube_event($record);
+ }
+
+}
diff --git a/calendar/drivers/ldap/resources_driver_ldap.php b/calendar/drivers/ldap/resources_driver_ldap.php
new file mode 100644
index 0000000..c377393
--- /dev/null
+++ b/calendar/drivers/ldap/resources_driver_ldap.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * LDAP-based resource directory class using rcube_ldap functionality
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * LDAP-based resource directory implementation
+ */
+class resources_driver_ldap extends resources_driver
+{
+ private $rc;
+ private $ldap;
+
+ /**
+ * Default constructor
+ */
+ function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ }
+
+ /**
+ * Fetch resource objects to be displayed for booking
+ *
+ * @param string Search query (optional)
+ * @return array List of resource records available for booking
+ */
+ public function load_resources($query = null, $num = 5000)
+ {
+ if (!($ldap = $this->connect())) {
+ return array();
+ }
+
+ // TODO: apply paging
+ $ldap->set_pagesize($num);
+
+ if (isset($query)) {
+ $results = $ldap->search('*', $query, 0, true, true);
+ }
+ else {
+ $results = $ldap->list_records();
+ }
+
+ if ($results instanceof ArrayAccess) {
+ foreach ($results as $i => $rec) {
+ $results[$i] = $this->decode_resource($rec);
+ }
+ }
+
+ return $results;
+ }
+
+ /**
+ * Return properties of a single resource
+ *
+ * @param string Unique resource identifier
+ * @return array Resource object as hash array
+ */
+ public function get_resource($dn)
+ {
+ $rec = null;
+
+ if ($ldap = $this->connect()) {
+ $rec = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
+
+ if (!empty($rec)) {
+ $rec = $this->decode_resource($rec);
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ * Return properties of a resource owner
+ *
+ * @param string Owner identifier
+ * @return array Resource object as hash array
+ */
+ public function get_resource_owner($dn)
+ {
+ $owner = null;
+
+ if ($ldap = $this->connect()) {
+ $owner = $ldap->get_record(rcube_ldap::dn_encode($dn), true);
+ $owner['ID'] = rcube_ldap::dn_decode($owner['ID']);
+ unset($owner['_raw_attrib'], $owner['_type']);
+ }
+
+ return $owner;
+ }
+
+ /**
+ * Extract JSON-serialized attributes
+ */
+ private function decode_resource($rec)
+ {
+ $rec['ID'] = rcube_ldap::dn_decode($rec['ID']);
+
+ if (is_array($rec['attributes']) && $rec['attributes'][0]) {
+ $attributes = array();
+
+ foreach ($rec['attributes'] as $sattr) {
+ $attr = @json_decode($sattr, true);
+ $attributes += $attr;
+ }
+
+ $rec['attributes'] = $attributes;
+ }
+
+ // force $rec['members'] to be an array
+ if (!empty($rec['members']) && !is_array($rec['members'])) {
+ $rec['members'] = array($rec['members']);
+ }
+
+ // remove unused cruft
+ unset($rec['_raw_attrib']);
+
+ return $rec;
+ }
+
+ private function connect()
+ {
+ if (!isset($this->ldap)) {
+ $this->ldap = new rcube_ldap($this->rc->config->get('calendar_resources_directory'), true);
+ }
+
+ return $this->ldap->ready ? $this->ldap : null;
+ }
+
+} \ No newline at end of file
diff --git a/calendar/drivers/resources_driver.php b/calendar/drivers/resources_driver.php
new file mode 100644
index 0000000..c51e922
--- /dev/null
+++ b/calendar/drivers/resources_driver.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Resources directory interface definition
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * Interface definition for a resources directory driver classe
+ */
+abstract class resources_driver
+{
+ protected$cal;
+
+ /**
+ * Default constructor
+ */
+ function __construct($cal)
+ {
+ $this->cal = $cal;
+ }
+
+ /**
+ * Fetch resource objects to be displayed for booking
+ *
+ * @param string Search query (optional)
+ * @return array List of resource records available for booking
+ */
+ abstract public function load_resources($query = null);
+
+ /**
+ * Return properties of a single resource
+ *
+ * @param string Unique resource identifier
+ * @return array Resource object as hash array
+ */
+ abstract public function get_resource($id);
+
+ /**
+ * Return properties of a resource owner
+ *
+ * @param string Owner identifier
+ * @return array Resource object as hash array
+ */
+ public function get_resource_owner($id)
+ {
+ return null;
+ }
+
+ /**
+ * Get event data to display a resource's calendar
+ *
+ * The default implementation extracts the resource's email address
+ * and fetches free-busy data using the calendar backend driver.
+ *
+ * @param integer Event's new start (unix timestamp)
+ * @param integer Event's new end (unix timestamp)
+ * @return array A list of event objects (see calendar_driver specification)
+ */
+ public function get_resource_calendar($id, $start, $end)
+ {
+ $events = array();
+ $rec = $this->get_resource($id);
+ if ($rec && !empty($rec['email']) && $this->cal->driver) {
+ $fbtypemap = array(
+ calendar::FREEBUSY_BUSY => 'busy',
+ calendar::FREEBUSY_TENTATIVE => 'tentative',
+ calendar::FREEBUSY_OOF => 'outofoffice',
+ );
+
+ // if the backend has free-busy information
+ $fblist = $this->cal->driver->get_freebusy_list($rec['email'], $start, $end);
+ if (is_array($fblist)) {
+ foreach ($fblist as $slot) {
+ list($from, $to, $type) = $slot;
+ if ($type == calendar::FREEBUSY_FREE || $type == calendar::FREEBUSY_UNKNOWN) {
+ continue;
+ }
+ if ($from < $end && $to > $start) {
+ $event = array(
+ 'id' => sha1($id . $from . $to),
+ 'title' => $rec['name'],
+ 'start' => new DateTime('@' . $from),
+ 'end' => new DateTime('@' . $to),
+ 'status' => $fbtypemap[$type],
+ 'calendar' => '_resource',
+ );
+ $events[] = $event;
+ }
+ }
+ }
+ }
+
+ return $events;
+ }
+
+}
diff --git a/calendar/helpdocs/en_US/_static/_skin b/calendar/helpdocs/en_US/_static/_skin
new file mode 120000
index 0000000..0137289
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/_skin
@@ -0,0 +1 @@
+larry \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/_static/kolab/alarms-popup.png b/calendar/helpdocs/en_US/_static/kolab/alarms-popup.png
new file mode 120000
index 0000000..874a425
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/alarms-popup.png
@@ -0,0 +1 @@
+../larry/alarms-popup.png \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/_static/kolab/calendar-acl.png b/calendar/helpdocs/en_US/_static/kolab/calendar-acl.png
new file mode 100644
index 0000000..66834ed
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/calendar-acl.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/kolab/calendar-header.png b/calendar/helpdocs/en_US/_static/kolab/calendar-header.png
new file mode 100644
index 0000000..effd2a8
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/calendar-header.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/kolab/event-participants.png b/calendar/helpdocs/en_US/_static/kolab/event-participants.png
new file mode 100644
index 0000000..8e29484
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/event-participants.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/kolab/event-resize.png b/calendar/helpdocs/en_US/_static/kolab/event-resize.png
new file mode 120000
index 0000000..8f78496
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/event-resize.png
@@ -0,0 +1 @@
+../larry/event-resize.png \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/_static/kolab/itip-invitation.png b/calendar/helpdocs/en_US/_static/kolab/itip-invitation.png
new file mode 120000
index 0000000..228911b
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/itip-invitation.png
@@ -0,0 +1 @@
+../larry/itip-invitation.png \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/_static/kolab/itip-reply.png b/calendar/helpdocs/en_US/_static/kolab/itip-reply.png
new file mode 120000
index 0000000..ecb0556
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/kolab/itip-reply.png
@@ -0,0 +1 @@
+../larry/itip-reply.png \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/_static/larry/alarms-popup.png b/calendar/helpdocs/en_US/_static/larry/alarms-popup.png
new file mode 100644
index 0000000..642704f
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/alarms-popup.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/larry/calendar-acl.png b/calendar/helpdocs/en_US/_static/larry/calendar-acl.png
new file mode 100644
index 0000000..753644f
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/calendar-acl.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/larry/calendar-header.png b/calendar/helpdocs/en_US/_static/larry/calendar-header.png
new file mode 100644
index 0000000..109478a
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/calendar-header.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/larry/event-participants.png b/calendar/helpdocs/en_US/_static/larry/event-participants.png
new file mode 100644
index 0000000..f2205dd
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/event-participants.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/larry/event-resize.png b/calendar/helpdocs/en_US/_static/larry/event-resize.png
new file mode 100644
index 0000000..6e1f004
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/event-resize.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/larry/itip-invitation.png b/calendar/helpdocs/en_US/_static/larry/itip-invitation.png
new file mode 100644
index 0000000..dd84697
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/itip-invitation.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/_static/larry/itip-reply.png b/calendar/helpdocs/en_US/_static/larry/itip-reply.png
new file mode 100644
index 0000000..3e2bab7
--- /dev/null
+++ b/calendar/helpdocs/en_US/_static/larry/itip-reply.png
Binary files differ
diff --git a/calendar/helpdocs/en_US/importexport.rst b/calendar/helpdocs/en_US/importexport.rst
new file mode 100644
index 0000000..d037e92
--- /dev/null
+++ b/calendar/helpdocs/en_US/importexport.rst
@@ -0,0 +1,43 @@
+.. index:: Import, iCal
+.. _calendar-import:
+
+*************
+Import/Export
+*************
+
+Event data is usually exchanged using the standard |iCal|_ format
+which is supported for import and export.
+
+
+Importing Events
+----------------
+
+This is how to add events from an |iCal|_ (.ics) file:
+
+1. Click the *Import* toolbar button in the calendar view.
+2. Then select the file to import from your computer's hard drive.
+3. Select the calendar to import the events to.
+4. Select the threshold for old events to be imported.
+5. Click *Import* and wait for the upload to finish.
+
+The calendar view will be refreshed to display the newly imported events.
+Verify that the according calendar is active if you don't see them.
+
+
+.. index:: Export
+.. _calendar-export:
+
+Exporting Events
+----------------
+
+Events from your calendars can be exported and downloaded in the |iCal|_ format.
+
+1. Click the *Export* toolbar button in the calendar view.
+2. Select the calendar where events should be exported from.
+3. With the *Events from* selector you choose the time constraints for exporting.
+4. Click the *Export* button to start the export.
+5. Choose where to save the exported .ics file if prompted, otherwise check the "Downloads" folder on your computer.
+
+
+.. |iCal| replace:: iCalendar
+.. _iCal: https://en.wikipedia.org/wiki/ICalendar
diff --git a/calendar/helpdocs/en_US/index.rst b/calendar/helpdocs/en_US/index.rst
new file mode 100644
index 0000000..ce88216
--- /dev/null
+++ b/calendar/helpdocs/en_US/index.rst
@@ -0,0 +1,18 @@
+.. index:: Calendar
+.. _calendar:
+
+********
+Calendar
+********
+
+The *Calendar* gives you access to your personal and shared calendar and scheduling functions.
+
+.. toctree::
+ :maxdepth: 2
+
+ overview
+ manage
+ invitations
+ importexport
+ sharing
+
diff --git a/calendar/helpdocs/en_US/invitations.rst b/calendar/helpdocs/en_US/invitations.rst
new file mode 100644
index 0000000..f23c13a
--- /dev/null
+++ b/calendar/helpdocs/en_US/invitations.rst
@@ -0,0 +1,55 @@
+.. index:: Invitation, RSVP
+.. _calendar-invitations:
+
+Handle Event Invitations
+========================
+
+In chapter :ref:`calendar-event-participants` we have learned how to invite
+other people to an event. This will send out invitation emails to all the
+participants with the event data attached. That allows one to directly accept
+or decline an event invitation.
+
+
+Receive Event Invitations
+-------------------------
+
+When the webmail system opens an invitation email with event data attached, it'll
+display a yellow box in the preview pane or the email view:
+
+.. image:: _static/_skin/itip-invitation.png
+
+Accept/Decline Invitations
+--------------------------
+
+Right in the box shown above, you can accept or decline the invitation by clicking the according
+button. This will send an automated response to the event organizer informing her about your
+decision and letting her update your participant status in her calendar.
+
+In case you accept, by either clicking *Accept* or *Maybe*, this will also copy
+the event into your personal calendar. The selector right next to the buttons lets you
+choose the right one.
+
+The copy in your calendar now knows about the invitation and its original sender. If you now
+delete it from your calendar, you'll be asked whether this should send a declination
+message to the person who organizes the event.
+
+After acceping or declining, the email message containing the invitation can be deleted.
+
+
+Process Invitation Replies
+--------------------------
+
+As an organizer who has invited others to an event, you'll receive responses to the
+automatically sent invitations when the attendees either accept or decline them.
+
+Such messages are also identified by the webmail system and again a yellow box appears
+in the message view:
+
+.. image:: _static/_skin/itip-reply.png
+
+By clicking the *Update the participant's status* button, the original event in
+your calendar will be updated with the RSVP status from the person who responded here.
+
+When you now look at the event details in the calendar view, the status icons next
+to each participant now displays the new status.
+
diff --git a/calendar/helpdocs/en_US/manage.rst b/calendar/helpdocs/en_US/manage.rst
new file mode 100644
index 0000000..36292a5
--- /dev/null
+++ b/calendar/helpdocs/en_US/manage.rst
@@ -0,0 +1,169 @@
+.. _calendar-manage:
+
+Manage Your Schedule
+====================
+
+All functions to maintain your events are accessible from the main calendar view.
+
+
+Add Events to a Calendar
+------------------------
+
+**Via toolbar**
+ Click the *New event* button in the toolbar to get an empty dialog where you enter
+ the :ref:`event properties <calendar-edit-event>` such as summary, date/time, reminders, etc.
+ Click *Save* to finally add it to the selected calendar.
+
+**At a specific date/time**
+ Navigate the calendar view to the date you want to add an event for. Then mark the range
+ of time (or dates in month view) with the mouse by pressing the button at the time the event
+ should start and releasing it again at time it finishes. This will open the :ref:`event dialog <calendar-edit-event>`
+ with the selected date/time range already filled in.
+
+ In order to create new all-day events, double-click the desired day in the calendar view.
+
+
+.. index:: Recurring events, Participants, Participants
+.. _calendar-edit-event:
+
+Edit and Reschedule Events
+--------------------------
+
+The Event Dialog
+^^^^^^^^^^^^^^^^
+
+When clicking an event in the calendar view, a dialog showing its details is displayed.
+Clicking the *Edit* button in that dialog opens the form to edit all properties of the selected event.
+
+The edit form is divided into different section which can be switched using the tabs on top
+of the dialog:
+
+**Summary**
+
+This general section has text fields and selectors for various properties of an event.
+Here's a description of all the possible values:
+
+* ``Summary``: The title of the event. This is what you will see in the calendar view.
+* ``Location``: Where the event will be taking place.
+* ``Description``: Any text that describes the event.
+* ``URL``: A link to more information about this event.
+* ``Start``: Date and time when the event starts.
+* ``End``: Date and time when the event starts.
+* ``all-day``: Check this if the event has no start/end time.
+* ``Reminder``: Will pop up with an notification at a the specified time before the event.
+* ``Calendar``: The calendar the event is saved in. Change it to move an event from one calendar to another.
+* ``Category``: The type of event. Categories can also be used for :ref:`coloring <settings-calendar>`.
+* ``Show me as``: The representation in your free/busy scheduling calendar visible to others.
+* ``Priority``: The priority value of the event.
+* ``Privacy``: Flag an event as "private" or "confidential" when sharing your calendar with others.
+
+**Recurrence**
+
+For periodically recurring event series, this tabs has the settings how an event is repeated
+over time.
+
+* ``Repeat``: Start with selecting a repetition interval (e.g. monthly)
+* ``Every``: How often the frequency will be relevant. For example, for an event that takes place every other week choose Weekly and then 2.
+ If you choose a frequency of weekly or monthly you can select which days of the week or month the event will occur.
+* ``Until``: Determines the duration of the repetition. The recurrence can either run forever, for a number it times or until a specific date.
+
+**Participants**
+
+An important part of managing your schedule is to invite others to events and track their RSVP.
+In this part of the edit dialog you can manage the participants of an event. Read more about this
+further down in the :ref:`calendar-event-participants` section.
+
+**Attachments**
+
+Sometimes a description text isn't enough to collect information for a specific event.
+Switch to this tab to attach files to the current event or to remove them again. Adding
+files works pretty much the same as :ref:`attaching them to email messages <mail-compose-attachments>`:
+first select a file from your local disk and click *Upload* in order to attach it.
+
+.. note:: Don't forget to finally save the changes by clicking *Save* in the event edit dialog.
+ Even switching back and forth the tabs will not yet save the data.
+
+
+.. index:: Move, Drag & Drop
+
+Moving and Resizing with the Mouse
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. container:: image-right
+
+ .. image:: _static/_skin/event-resize.png
+
+ If an existing event shall be rescheduled to another time or date, you'll find it handy
+ to do that directly in the calendar view without opening the edit form. Simply grab the event
+ block with the mouse and move it to the new date or time. Release the mouse button to complete.
+
+ In *Month* and *Day* view, the event blocks have a small handle at the bottom. Drag this with the
+ mouse in order to resize the event meaning to adjust its duration.
+
+
+.. index:: Notifications, Reminders, Alarms
+.. _calendar-event-alarms:
+
+Get Notifications
+-----------------
+
+.. container:: image-right
+
+ .. image:: _static/_skin/alarms-popup.png
+
+ While logged in to the webmail, event reminders will be displayed with pop-up boxes at the specified time
+ before the event starts. You can specify if you want to see alarms for every calendar individually.
+ Enable or disable reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the :ref:`calendar-lists`.
+
+Dismiss or Snooze Reminders
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When a reminder box pops up, you can either dismiss the notification for all events or each one individually.
+When dismissed, no further reminders will be displayed. Choose a time from the *Snooze* menu to get another
+reminder after the selected time.
+
+
+.. index:: Invite, Participants, Attendees
+.. _calendar-event-participants:
+
+Inviting Other People
+---------------------
+
+If you need to set up a meeting, and keep track of who's attending and who is not, the calendar can do this
+as well as you to automatically send invitations and read their responses.
+
+When creating a new event, switch to the *Participants* tab. You're already listed as the organizer of the event.
+
+.. image:: _static/_skin/event-participants.png
+
+1. Enter the name or email address of the person to invite. Contacts from the address book are suggested as you type.
+ In order to send invitations, make sure the entered contact has an email address. Type it in the form
+ ``Person Name <email@address.com>``.
+2. Click *Add participant* to add the person to the list.
+3. Select a *Role* (e.g. required or optional) for this person.
+4. Repeat 1-3 for further participants.
+5. Check the *Send invitations* box if the application should send out invitation emails.
+
+Invitations will be sent out when you click *Save* and the event is created.
+
+.. only:: kolab
+
+ .. index:: Availability
+ .. _calendar-availability-finder:
+
+ Find Availability
+ ^^^^^^^^^^^^^^^^^
+
+ Once all the participants are added to the list, you see the individual availability status for each one
+ of them, given that this information is available. In case not everybody is free, click the *Find availability...*
+ button to open the scheduling dialog. In that dialog, detailed availability information for all participants is
+ displayed. Use the *Previous/Next Slot* buttons to find the next time slot where all required participants are
+ available. Or drag the gray area representing the event duration with the mouse to manually select a free slot.
+
+ Click *Select* to copy the rescheduled date/time back into the event form and to close this dialog.
+
+
+Receive Event Invitations
+-------------------------
+
+How to process incoming event invitations is described in chapter :ref:`calendar-invitations`. \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/overview.rst b/calendar/helpdocs/en_US/overview.rst
new file mode 100644
index 0000000..b383319
--- /dev/null
+++ b/calendar/helpdocs/en_US/overview.rst
@@ -0,0 +1,138 @@
+*************
+Overview
+*************
+
+The screen of the calendar module presents the following parts: the :ref:`Calendar View <calendar-view>`
+itself, a small :ref:`Calendar Widget <calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual
+toolbar and search box.
+
+
+.. index:: Month View, Week View, Day View, Agenda
+.. _calendar-view:
+
+Calendar View
+=============
+
+The central part of the screen displays the schedule with events from the active calendars matching the current
+date range. The active date range is displayed above the calendar in the toolbar area and can be moved forward or
+backward in time using the arrow buttons right next to the title.
+
+.. image:: _static/_skin/calendar-header.png
+
+
+Change Views
+------------
+
+You can view your calendar events in Day, Week, Month or Agenda view. Toggle the view mode using the toolbar buttons
+above the calendar view.
+
+**Day**
+ All events of a single day appear at the time the begin and spawn a box until their end time. The time
+ scale is displayed in the left side of the view. All-day events appear at the top.
+
+**Week**
+ Similar to the day view but lists all days of the week horizontally. All-day events again appear at the top.
+
+**Month**
+ Shows all events of the selected month at a time. Each event only appears as a single line and if there are
+ more events in a day than can be listed, a number at the bottom of the day field indicates that. Click that
+ link to open a zoomed view of that single day.
+
+**Agenda**
+ The agenda view shows a list of events for the selected range in a chronological order and divided by
+ headers denoting either days, weeks or months. Both the number of the days considered for the listing as well
+ as the mode how to divide list can be adjusted with the controls at the bottom of the agenda view.
+
+.. _calendar-minicalendar:
+
+For all the views, the small calendar on the left highlights the currently listed days.
+
+Go to a specific Date
+---------------------
+
+Use the mini calendar widget on the left to jump to a specific date. Simply click a date and the date range of the current
+view moves to include the selected day. The left/right arrows in the mini calendar's header quickly cycle through the months.
+Use the drop-down menus hidden under the month and year display in the widget header to directly jump to another month or year.
+
+A shortcut to switch the calendar view back to today or the current week provides the *Today* button located in the toolbar.
+
+
+Show Event Details
+------------------
+
+Click an event box in the calendar view to open a dialog displaying all details of the event.
+
+
+Searching Events
+----------------
+
+The search box above the calendar view lets you quickly get a list of events matching the entered keyword
+in either the title, location, description or attendees. Enter the search term into the box and press <Enter>
+on your keyboard to start the search. The calendar view will switch to *Agenda* mode in order to display
+a list of matches. Of course you can switch the view again to display the search results differently.
+
+.. note:: Events are searched within a certain date reange only which is displayed above the calendar view.
+ Use the mini calendar widget or the arrow toolbar buttons and the range selector below the agenda view
+ to adjust the time frame to search in.
+
+For searching as well as for normal views, only events from active calendars are displayed. Use the checkboxes
+in the :ref:`calendar-lists` to add or hide events from different calendars.
+
+Reset the search by clicking the *Reset search* icon on the right border of the search box. This will
+also switch the calendar view to whatever mode you had before the search.
+
+
+
+.. index:: Calendars
+.. _calendar-lists:
+
+Calendars List
+==============
+
+Events can be organized in different calendars which are all displayed in the lower left list.
+Use the checkboxes in that list to show or hide events from the specific calendars in the main view.
+
+.. only:: kolab
+
+ Beside your personal calendars, the list also displays calendars shared by other users
+ or ones that are shared amongst your workgroup. Small icons in the list give a hint
+ about the origin and some of them are possibly read-only which is denoted with a small lock icon.
+
+
+Colorized Events
+----------------
+
+In order to better distinguish the events from various calendars in the calendar view, calendars have
+a color assigned which is used to colorize the events on the screen. Check the :ref:`settings-calendar`
+for more advanced options how to colorize events in the calendar view.
+
+You can create any number of calendars to store all your events and name them individually.
+
+
+Create a New Calendar
+---------------------
+
+1. Click the + icon in the calendars list footer.
+2. In the dialog, give the new calendar a unique name and assign a color.
+3. Click *Save* to create it.
+
+The calendar view will reload and list the new calendar on the left.
+
+.. _calendar-edit-properties:
+
+Edit Calendar Names and Settings
+--------------------------------
+
+1. Select the calendar to edit by clicking it in the list.
+2. Click the gear icon in the calendars list footer and select *Edit* from the options menu.
+3. Adjust name, color or reminders settings in the edit dialog.
+4. Click *Save* to finally update the calendar.
+
+Remove entire Calendars
+-----------------------
+
+1. Select the calendar to edit by clicking it in the list.
+2. Click the gear icon in the calendars list footer and select *Remove* from the options menu.
+3. After a confirmation dialog, the selected calendar with all its events will be deleted.
+ Caution: This action cannot be undone!
+ \ No newline at end of file
diff --git a/calendar/helpdocs/en_US/settings.rst b/calendar/helpdocs/en_US/settings.rst
new file mode 100644
index 0000000..154e6e8
--- /dev/null
+++ b/calendar/helpdocs/en_US/settings.rst
@@ -0,0 +1,75 @@
+.. _settings-calendar:
+
+********************
+Calendar Preferences
+********************
+
+The settings for the calendar module are listed in *Settings > Preferences* and are grouped by the following sections:
+
+
+Main Options
+------------
+
+**Default view**
+ Lets you select the :ref:`calendar-view` which is visible by default when opening the calendar.
+
+**Time slots per hour**
+ How one hour in day and week view is divided vertically. If for example set to 2, you will see
+ events displayed in 30 minute blocks.
+
+**First weekday**
+ Which weekday to begin the week view with.
+
+**First hour to show**
+ When opening the day or week view, the listing of events starts at this time. Of course all
+ hours of a day are visible by scrolling further up.
+
+.. only:: kolab
+
+ **Working hours**
+ This time range will be used in the :ref:`availability finder <calendar-availability-finder>`
+ when automatically selecting free slots for a meeting.
+
+**Event coloring**
+ The coloring of the title of an event block ("outline") as well as the background color of the box ("content")
+ in day and week views is influenced by the color of the calendar an event belongs to and/or the color of the
+ category it is assigned to. This setting lets you control which source for coloring to use or if you even
+ want a combined coloring that reflects both, the assignment of calendars and categories.
+
+**Default reminder setting**
+ When creating new events, they'll have this type of reminder set by default.
+
+**Default reminder time**
+ When enabling reminders in a new event, use this preset as default.
+
+**Create new events in**
+ This is the default selection for saving new events. Used in both the calendar view and when
+ accepting event invitations.
+
+
+Categories
+----------
+
+This block allows the management of categories used in your calendar and assign colors to them.
+Use the color picker to change the color by clicking on the square color box in the categories list.
+
+To add a new category, enter its unique name into the text box below the listing and then click
+the *Add category** button to add it. Note that you still need to click the *Save* button
+at the bottom of the preferences panel in order to finally register the new categories.
+
+
+Birthdays Calendar
+------------------
+
+The calendar view and also display birthdays from contacts saved in your address book. This
+block controls how this is done.
+
+**Display birthdays calendar**
+ Enable the birthdays calendar feature with this checkbox.
+
+**From these address books**
+ Choose from which address books you'd like to see birthdays in your calendar.
+
+**Show reminders**
+ This option controls whether and when to display reminder notifications for
+ upcoming birthdays.
diff --git a/calendar/helpdocs/en_US/sharing.rst b/calendar/helpdocs/en_US/sharing.rst
new file mode 100644
index 0000000..34599df
--- /dev/null
+++ b/calendar/helpdocs/en_US/sharing.rst
@@ -0,0 +1,49 @@
+.. _calendar-sharing:
+
+.. only:: kolab
+
+ .. index:: Sharing
+
+ Sharing Calendars
+ =================
+
+ For collaboration, sharing calendars is an important feature. In the :ref:`overview <calendar-lists>`,
+ we have already learned how calendars others share with you appear in the calendars list. The following
+ now explains how to make personal calendars accessible to fellow users.
+
+
+ Share a Calendar with others
+ ----------------------------
+
+ Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-edit-properties>`.
+ Double-click a calendar in the list on the left and then select the *Sharing* tab at the top of
+ the dialog box:
+
+ .. image:: _static/_skin/calendar-acl.png
+
+ The table displays who already has permission to see and modify the selected calendar.
+ In order to share the calendar with a new user do
+
+ 1. Click the *Add entry* button (+) in the table footer
+ 2. Enter the username or choose one from the autocompletion menu that appears when you start typing.
+ Instead of a specific user, permissons can be granted for all users or guests.
+ 3. Select the access rights you want to grant for the user
+ 4. Click *Save* to add the permission
+
+ Double-click an entry to edit the permissions for a particular user or group.
+
+ For removing existing permissions, select the according entry in the list and then choose
+ *Delete* from the menu behind the gear icon in the footer of the list.
+
+
+ Subscribe to Shared Calendars
+ -----------------------------
+
+ Calendars shared by others are not showing up right away in the list within the calendar view.
+ Switch to :ref:`Settings > Folders <settings-folders>` to see all resources you can access.
+ There's a shortcut to this: click *Manage folders* in the options menu behind the gear icon
+ located the footer of the calendars list.
+
+ In order to make a shared calendar appear in the calendars list, locate it in the folder manager
+ and check the *Subscribed* mark in the list. Only subscribed calendars are visible in the calendar view.
+
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..726ddbe
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/index.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/index.po
new file mode 100644
index 0000000..5b6acc8
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..ab66c11
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..517e15e
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..ea5205e
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..380c134
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..e884f79
--- /dev/null
+++ b/calendar/helpdocs/locale/bg_BG/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Bulgarian (Bulgaria) (http://www.transifex.com/projects/p/kolab-documentation/language/bg_BG/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: bg_BG\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..d3f5efe
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/index.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/index.po
new file mode 100644
index 0000000..66c94d0
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..572898c
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..d542da8
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..23fd838
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..c70eaf8
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..9abf7ef
--- /dev/null
+++ b/calendar/helpdocs/locale/ca_ES/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Catalan (Spain) (http://www.transifex.com/projects/p/kolab-documentation/language/ca_ES/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ca_ES\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..5ff7464
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/index.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/index.po
new file mode 100644
index 0000000..00733ad
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..96cef09
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..594b325
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..d27fcf9
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..a07945e
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..47810cd
--- /dev/null
+++ b/calendar/helpdocs/locale/cs_CZ/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/kolab-documentation/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..db14824
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/index.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/index.po
new file mode 100644
index 0000000..234f3c7
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..b1df490
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..e25a6e3
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..dcc4fe8
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..a20359f
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/da_DK/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..e871796
--- /dev/null
+++ b/calendar/helpdocs/locale/da_DK/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/kolab-documentation/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..d3c56f8
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/index.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/index.po
new file mode 100644
index 0000000..e4c169b
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..c118c86
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..8691b86
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..da38732
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..b3ba9cc
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_CH/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..c56232f
--- /dev/null
+++ b/calendar/helpdocs/locale/de_CH/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: German (Switzerland) (http://www.transifex.com/projects/p/kolab-documentation/language/de_CH/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de_CH\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..9db9662
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/importexport.po
@@ -0,0 +1,97 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:47+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr "Import/Export"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr "Termine werden importiert"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr "Klicken Sie in der Kalenderansicht auf die Werkzeugleistenschaltfläche *Import*."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr "Wählen Sie den Kalender zum Importieren der Termine aus."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr "Klicken Sie auf *Import* und warten Sie, bis das Hochladen abgeschlossen ist."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr "Termine werden exportiert"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr "Klicken Sie in der Kalenderansicht auf die Werkzeugleistenschaltfläche *Export*."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr "Klicken Sie auf die Schaltfläche *Export*, um den Export zu starten."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/index.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/index.po
new file mode 100644
index 0000000..800333a
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/index.po
@@ -0,0 +1,29 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:47+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr "Kalender"
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr "Der *Kalender* ermöglicht Ihnen den Zugriff auf Ihren persönlichen und den gemeinsamen Kalender sowie auf Planungsfunktionen."
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..ce825a1
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/invitations.po
@@ -0,0 +1,104 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-28 00:16+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr "Termineinladungen erhalten"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr "Einladungen annehmen/ablehnen"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr "Einladungsantworten abarbeiten"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..3e6abcc
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/manage.po
@@ -0,0 +1,343 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-28 00:17+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr "Termine zum Kalender hinzufügen"
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr "Termine bearbeiten und verlegen"
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr "Der Termindialog"
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr "**Zusammenfassung**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr "**Teilnehmer**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr "**Anhänge**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr "Benachrichtigungen holen"
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr "Termineinladungen erhalten"
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..2d8c8e2
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/overview.po
@@ -0,0 +1,267 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-28 00:17+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr "Übersicht"
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr "Kalenderansicht"
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr "Ansichten ändern"
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr "**Tag**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr "**Woche**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr "**Monat**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr "**Tagesordnung**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr "Zu einem bestimmten Datum gehen"
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr "Termindetails anzeigen"
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr "Terminsuche"
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr "Kalenderliste"
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr "Eingefärbte Termine"
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr "Einen neuen Kalender erstellen"
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr "Klicken Sie auf *Speichern* zum Erstellen."
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr "Kalendernamen und Einstellungen bearbeiten"
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr "Gesamte Kalender entfernen"
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..9ceab84
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/settings.po
@@ -0,0 +1,179 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-28 00:17+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr "Kalendereinstellungen"
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr "Hauptoptionen"
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr "**Standardansicht**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr "**Zeitfenster pro Stunde**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr "**Erster Wochentag**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr "**Erste angezeigte Stunde**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr "**Arbeitszeiten**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr "**Einfärbung der Termine**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr "**Standard-Erinnerungseinstellung**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr "**Standard-Erinnerungszeit**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr "**Neue Termine erstellen in**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr "Kategorien"
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr "Geburtstagskalender"
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr "**Geburtstagskalender anzeigen**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr "**Von diesen Adressbüchern**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr "**Erinnerungen anzeigen**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/de_DE/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..721eb4c
--- /dev/null
+++ b/calendar/helpdocs/locale/de_DE/LC_MESSAGES/sharing.po
@@ -0,0 +1,100 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+# Ettore Atalan <atalanttore@googlemail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-28 00:17+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/kolab-documentation/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"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr "Geteilte Kalender"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr "Kalender mit Anderen teilen"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr "Klicken Sie auf die Schaltfläche *Eintrag hinzufügen* (+) im Tabellenfuß"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr "Klicken Sie auf *Speichern* zum Hinzufügen der Berechtigung."
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr "Geteilte Kalender abonnieren"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..d01852b
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr "Import/Export"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr "Event data is usually exchanged using the standard |iCal|_ format which is supported for import and export."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr "Importing Events"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr "This is how to add events from an |iCal|_ (.ics) file:"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr "Click the *Import* toolbar button in the calendar view."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr "Then select the file to import from your computer's hard drive."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr "Select the calendar to import the events to."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr "Select the threshold for old events to be imported."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr "Click *Import* and wait for the upload to finish."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr "The calendar view will be refreshed to display the newly imported events. Verify that the according calendar is active if you don't see them."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr "Exporting Events"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr "Events from your calendars can be exported and downloaded in the |iCal|_ format."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr "Click the *Export* toolbar button in the calendar view."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr "Select the calendar where events should be exported from."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr "With the *Events from* selector you choose the time constraints for exporting."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr "Click the *Export* button to start the export."
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr "Choose where to save the exported .ics file if prompted, otherwise check the \"Downloads\" folder on your computer."
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/index.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/index.po
new file mode 100644
index 0000000..c74939b
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr "Calendar"
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr "The *Calendar* gives you access to your personal and shared calendar and scheduling functions."
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..e5f10a9
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr "Handle Event Invitations"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr "In chapter :ref:`calendar-event-participants` we have learned how to invite other people to an event. This will send out invitation emails to all the participants with the event data attached. That allows one to directly accept or decline an event invitation."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr "Receive Event Invitations"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr "When the webmail system opens an invitation email with event data attached, it'll display a yellow box in the preview pane or the email view:"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr "Accept/Decline Invitations"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr "Right in the box shown above, you can accept or decline the invitation by clicking the according button. This will send an automated response to the event organizer informing her about your decision and letting her update your participant status in her calendar."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr "In case you accept, by either clicking *Accept* or *Maybe*, this will also copy the event into your personal calendar. The selector right next to the buttons lets you choose the right one."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr "The copy in your calendar now knows about the invitation and its original sender. If you now delete it from your calendar, you'll be asked whether this should send a declination message to the person who organizes the event."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr "After acceping or declining, the email message containing the invitation can be deleted."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr "Process Invitation Replies"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr "As an organizer who has invited others to an event, you'll receive responses to the automatically sent invitations when the attendees either accept or decline them."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr "Such messages are also identified by the webmail system and again a yellow box appears in the message view:"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr "By clicking the *Update the participant's status* button, the original event in your calendar will be updated with the RSVP status from the person who responded here."
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr "When you now look at the event details in the calendar view, the status icons next to each participant now displays the new status."
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..7452685
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr "Manage Your Schedule"
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr "All functions to maintain your events are accessible from the main calendar view."
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr "Add Events to a Calendar"
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr "**Via toolbar**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr "Click the *New event* button in the toolbar to get an empty dialog where you enter the :ref:`event properties <calendar-edit-event>` such as summary, date/time, reminders, etc. Click *Save* to finally add it to the selected calendar."
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr "**At a specific date/time**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr "Navigate the calendar view to the date you want to add an event for. Then mark the range of time (or dates in month view) with the mouse by pressing the button at the time the event should start and releasing it again at time it finishes. This will open the :ref:`event dialog <calendar-edit-event>` with the selected date/time range already filled in."
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr "In order to create new all-day events, double-click the desired day in the calendar view."
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr "Edit and Reschedule Events"
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr "The Event Dialog"
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr "When clicking an event in the calendar view, a dialog showing its details is displayed. Clicking the *Edit* button in that dialog opens the form to edit all properties of the selected event."
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr "The edit form is divided into different section which can be switched using the tabs on top of the dialog:"
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr "**Summary**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr "This general section has text fields and selectors for various properties of an event. Here's a description of all the possible values:"
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr "``Summary``: The title of the event. This is what you will see in the calendar view."
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr "``Location``: Where the event will be taking place."
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr "``Description``: Any text that describes the event."
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr "``URL``: A link to more information about this event."
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr "``Start``: Date and time when the event starts."
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr "``End``: Date and time when the event starts."
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr "``all-day``: Check this if the event has no start/end time."
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr "``Reminder``: Will pop up with an notification at a the specified time before the event."
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr "``Calendar``: The calendar the event is saved in. Change it to move an event from one calendar to another."
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr "``Category``: The type of event. Categories can also be used for :ref:`coloring <settings-calendar>`."
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr "``Show me as``: The representation in your free/busy scheduling calendar visible to others."
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr "``Priority``: The priority value of the event."
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr "``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing your calendar with others."
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr "**Recurrence**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr "For periodically recurring event series, this tabs has the settings how an event is repeated over time."
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr "``Every``: How often the frequency will be relevant. For example, for an event that takes place every other week choose Weekly and then 2. If you choose a frequency of weekly or monthly you can select which days of the week or month the event will occur."
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr "``Until``: Determines the duration of the repetition. The recurrence can either run forever, for a number it times or until a specific date."
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr "**Participants**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr "An important part of managing your schedule is to invite others to events and track their RSVP. In this part of the edit dialog you can manage the participants of an event. Read more about this further down in the :ref:`calendar-event-participants` section."
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr "**Attachments**"
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr "Sometimes a description text isn't enough to collect information for a specific event. Switch to this tab to attach files to the current event or to remove them again. Adding files works pretty much the same as :ref:`attaching them to email messages <mail-compose-attachments>`: first select a file from your local disk and click *Upload* in order to attach it."
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr "Don't forget to finally save the changes by clicking *Save* in the event edit dialog. Even switching back and forth the tabs will not yet save the data."
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr "Moving and Resizing with the Mouse"
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr "If an existing event shall be rescheduled to another time or date, you'll find it handy to do that directly in the calendar view without opening the edit form. Simply grab the event block with the mouse and move it to the new date or time. Release the mouse button to complete."
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr "In *Month* and *Day* view, the event blocks have a small handle at the bottom. Drag this with the mouse in order to resize the event meaning to adjust its duration."
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr "Get Notifications"
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr "While logged in to the webmail, event reminders will be displayed with pop-up boxes at the specified time before the event starts. You can specify if you want to see alarms for every calendar individually. Enable or disable reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the :ref:`calendar-lists`."
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr "Dismiss or Snooze Reminders"
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr "When a reminder box pops up, you can either dismiss the notification for all events or each one individually. When dismissed, no further reminders will be displayed. Choose a time from the *Snooze* menu to get another reminder after the selected time."
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr "Inviting Other People"
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr "If you need to set up a meeting, and keep track of who's attending and who is not, the calendar can do this as well as you to automatically send invitations and read their responses."
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr "When creating a new event, switch to the *Participants* tab. You're already listed as the organizer of the event."
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr "Enter the name or email address of the person to invite. Contacts from the address book are suggested as you type. In order to send invitations, make sure the entered contact has an email address. Type it in the form ``Person Name <email@address.com>``."
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr "Click *Add participant* to add the person to the list."
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr "Select a *Role* (e.g. required or optional) for this person."
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr "Repeat 1-3 for further participants."
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr "Check the *Send invitations* box if the application should send out invitation emails."
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr "Invitations will be sent out when you click *Save* and the event is created."
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr "Find Availability"
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr "Once all the participants are added to the list, you see the individual availability status for each one of them, given that this information is available. In case not everybody is free, click the *Find availability...* button to open the scheduling dialog. In that dialog, detailed availability information for all participants is displayed. Use the *Previous/Next Slot* buttons to find the next time slot where all required participants are available. Or drag the gray area representing the event duration with the mouse to manually select a free slot."
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr "Click *Select* to copy the rescheduled date/time back into the event form and to close this dialog."
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr "Receive Event Invitations"
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr "How to process incoming event invitations is described in chapter :ref:`calendar-invitations`."
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..ae4b735
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr "Overview"
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr "The screen of the calendar module presents the following parts: the :ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget <calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual toolbar and search box."
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr "Calendar View"
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr "The central part of the screen displays the schedule with events from the active calendars matching the current date range. The active date range is displayed above the calendar in the toolbar area and can be moved forward or backward in time using the arrow buttons right next to the title."
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr "Change Views"
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr "You can view your calendar events in Day, Week, Month or Agenda view. Toggle the view mode using the toolbar buttons above the calendar view."
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr "**Day**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr "All events of a single day appear at the time the begin and spawn a box until their end time. The time scale is displayed in the left side of the view. All-day events appear at the top."
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr "**Week**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr "Similar to the day view but lists all days of the week horizontally. All-day events again appear at the top."
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr "**Month**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr "Shows all events of the selected month at a time. Each event only appears as a single line and if there are more events in a day than can be listed, a number at the bottom of the day field indicates that. Click that link to open a zoomed view of that single day."
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr "**Agenda**"
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr "The agenda view shows a list of events for the selected range in a chronological order and divided by headers denoting either days, weeks or months. Both the number of the days considered for the listing as well as the mode how to divide list can be adjusted with the controls at the bottom of the agenda view."
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr "For all the views, the small calendar on the left highlights the currently listed days."
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr "Go to a specific Date"
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr "Use the mini calendar widget on the left to jump to a specific date. Simply click a date and the date range of the current view moves to include the selected day. The left/right arrows in the mini calendar's header quickly cycle through the months. Use the drop-down menus hidden under the month and year display in the widget header to directly jump to another month or year."
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr "A shortcut to switch the calendar view back to today or the current week provides the *Today* button located in the toolbar."
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr "Show Event Details"
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr "Click an event box in the calendar view to open a dialog displaying all details of the event."
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr "Searching Events"
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr "The search box above the calendar view lets you quickly get a list of events matching the entered keyword in either the title, location, description or attendees. Enter the search term into the box and press <Enter> on your keyboard to start the search. The calendar view will switch to *Agenda* mode in order to display a list of matches. Of course you can switch the view again to display the search results differently."
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr "Events are searched within a certain date reange only which is displayed above the calendar view. Use the mini calendar widget or the arrow toolbar buttons and the range selector below the agenda view to adjust the time frame to search in."
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr "For searching as well as for normal views, only events from active calendars are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or hide events from different calendars."
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr "Reset the search by clicking the *Reset search* icon on the right border of the search box. This will also switch the calendar view to whatever mode you had before the search."
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr "Calendars List"
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr "Events can be organized in different calendars which are all displayed in the lower left list. Use the checkboxes in that list to show or hide events from the specific calendars in the main view."
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr "Beside your personal calendars, the list also displays calendars shared by other users or ones that are shared amongst your workgroup. Small icons in the list give a hint about the origin and some of them are possibly read-only which is denoted with a small lock icon."
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr "Colorized Events"
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr "In order to better distinguish the events from various calendars in the calendar view, calendars have a color assigned which is used to colorize the events on the screen. Check the :ref:`settings-calendar` for more advanced options how to colorize events in the calendar view."
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr "You can create any number of calendars to store all your events and name them individually."
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr "Create a New Calendar"
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr "Click the + icon in the calendars list footer."
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr "In the dialog, give the new calendar a unique name and assign a color."
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr "Click *Save* to create it."
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr "The calendar view will reload and list the new calendar on the left."
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr "Edit Calendar Names and Settings"
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr "Select the calendar to edit by clicking it in the list."
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr "Click the gear icon in the calendars list footer and select *Edit* from the options menu."
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr "Adjust name, color or reminders settings in the edit dialog."
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr "Click *Save* to finally update the calendar."
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr "Remove entire Calendars"
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr "Click the gear icon in the calendars list footer and select *Remove* from the options menu."
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr "After a confirmation dialog, the selected calendar with all its events will be deleted. Caution: This action cannot be undone!"
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..6d77d60
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr "Calendar Preferences"
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr "The settings for the calendar module are listed in *Settings > Preferences* and are grouped by the following sections:"
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr "Main Options"
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr "**Default view**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr "Lets you select the :ref:`calendar-view` which is visible by default when opening the calendar."
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr "**Time slots per hour**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr "How one hour in day and week view is divided vertically. If for example set to 2, you will see events displayed in 30 minute blocks."
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr "**First weekday**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr "Which weekday to begin the week view with."
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr "**First hour to show**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr "When opening the day or week view, the listing of events starts at this time. Of course all hours of a day are visible by scrolling further up."
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr "**Working hours**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr "This time range will be used in the :ref:`availability finder <calendar-availability-finder>` when automatically selecting free slots for a meeting."
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr "**Event coloring**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr "The coloring of the title of an event block (\"outline\") as well as the background color of the box (\"content\") in day and week views is influenced by the color of the calendar an event belongs to and/or the color of the category it is assigned to. This setting lets you control which source for coloring to use or if you even want a combined coloring that reflects both, the assignment of calendars and categories."
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr "**Default reminder setting**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr "When creating new events, they'll have this type of reminder set by default."
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr "**Default reminder time**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr "When enabling reminders in a new event, use this preset as default."
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr "**Create new events in**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr "This is the default selection for saving new events. Used in both the calendar view and when accepting event invitations."
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr "Categories"
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr "This block allows the management of categories used in your calendar and assign colors to them. Use the color picker to change the color by clicking on the square color box in the categories list."
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr "To add a new category, enter its unique name into the text box below the listing and then click the *Add category** button to add it. Note that you still need to click the *Save* button at the bottom of the preferences panel in order to finally register the new categories."
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr "Birthdays Calendar"
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr "The calendar view and also display birthdays from contacts saved in your address book. This block controls how this is done."
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr "**Display birthdays calendar**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr "Enable the birthdays calendar feature with this checkbox."
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr "**From these address books**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr "Choose from which address books you'd like to see birthdays in your calendar."
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr "**Show reminders**"
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr "This option controls whether and when to display reminder notifications for upcoming birthdays."
diff --git a/calendar/helpdocs/locale/en_US/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/en_US/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..fcf11b6
--- /dev/null
+++ b/calendar/helpdocs/locale/en_US/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/kolab-documentation/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr "Sharing Calendars"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr "For collaboration, sharing calendars is an important feature. In the :ref:`overview <calendar-lists>`, we have already learned how calendars others share with you appear in the calendars list. The following now explains how to make personal calendars accessible to fellow users."
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr "Share a Calendar with others"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr "Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-edit-properties>`. Double-click a calendar in the list on the left and then select the *Sharing* tab at the top of the dialog box:"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr "The table displays who already has permission to see and modify the selected calendar. In order to share the calendar with a new user do"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr "Click the *Add entry* button (+) in the table footer"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr "Enter the username or choose one from the autocompletion menu that appears when you start typing. Instead of a specific user, permissons can be granted for all users or guests."
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr "Select the access rights you want to grant for the user"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr "Click *Save* to add the permission"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr "Double-click an entry to edit the permissions for a particular user or group."
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr "For removing existing permissions, select the according entry in the list and then choose *Delete* from the menu behind the gear icon in the footer of the list."
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr "Subscribe to Shared Calendars"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr "Calendars shared by others are not showing up right away in the list within the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to see all resources you can access. There's a shortcut to this: click *Manage folders* in the options menu behind the gear icon located the footer of the calendars list."
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr "In order to make a shared calendar appear in the calendars list, locate it in the folder manager and check the *Subscribed* mark in the list. Only subscribed calendars are visible in the calendar view."
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..dfe07dc
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/index.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/index.po
new file mode 100644
index 0000000..35d8ac7
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..3fd29cb
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..edc7baa
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..445f3be
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..13d7e3a
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_AR/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..abdd70b
--- /dev/null
+++ b/calendar/helpdocs/locale/es_AR/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (Argentina) (http://www.transifex.com/projects/p/kolab-documentation/language/es_AR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es_AR\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..0395809
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/index.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/index.po
new file mode 100644
index 0000000..7b98eab
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..bdd005c
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..c3755a7
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..1821086
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..9a37c8b
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/es_ES/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..afdaa42
--- /dev/null
+++ b/calendar/helpdocs/locale/es_ES/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/kolab-documentation/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..d9f32ff
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/index.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/index.po
new file mode 100644
index 0000000..f71c900
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..5c4bc99
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..f241495
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..a98d361
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..ec08cf7
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/et_EE/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..be5931d
--- /dev/null
+++ b/calendar/helpdocs/locale/et_EE/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Estonian (Estonia) (http://www.transifex.com/projects/p/kolab-documentation/language/et_EE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et_EE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..7888d50
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/index.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/index.po
new file mode 100644
index 0000000..6c7d920
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..522d262
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..ad17ea1
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..d8a89a9
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..880c144
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..7018e77
--- /dev/null
+++ b/calendar/helpdocs/locale/fi_FI/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Finnish (http://www.transifex.com/projects/p/kolab-documentation/language/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..1dfa201
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/index.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/index.po
new file mode 100644
index 0000000..0104b6f
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:47+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr "Agenda"
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..13419d2
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..ef587a9
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..b1b789b
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..ce298c4
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-28 00:17+0000\n"
+"Last-Translator: Torsten Grote <grote@kolabsys.com>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr "Catégories"
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr "Calendrier des anniversaires"
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..518ade4
--- /dev/null
+++ b/calendar/helpdocs/locale/fr_FR/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/kolab-documentation/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/he/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..979de8a
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/index.po b/calendar/helpdocs/locale/he/LC_MESSAGES/index.po
new file mode 100644
index 0000000..227e78b
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/he/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..6608afb
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/he/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..c2da982
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/he/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..7b08c74
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/he/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..40977d0
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..78e77e3
--- /dev/null
+++ b/calendar/helpdocs/locale/he/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hebrew (http://www.transifex.com/projects/p/kolab-documentation/language/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: he\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..cfb6e0a
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/index.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/index.po
new file mode 100644
index 0000000..8647e57
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..dae9d0f
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..59f431a
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..07a0f9b
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..d3a9cfa
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..7045983
--- /dev/null
+++ b/calendar/helpdocs/locale/hr/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Croatian (http://www.transifex.com/projects/p/kolab-documentation/language/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..b1a90fa
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/index.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/index.po
new file mode 100644
index 0000000..8ed9463
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..bf23058
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..0cf720d
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..170553d
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..3490349
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..12961ee
--- /dev/null
+++ b/calendar/helpdocs/locale/hu_HU/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Hungarian (Hungary) (http://www.transifex.com/projects/p/kolab-documentation/language/hu_HU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu_HU\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..abcab50
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/index.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/index.po
new file mode 100644
index 0000000..5fcc411
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..4425d11
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..15e3a4c
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..8d2d120
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..96d784a
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/it_IT/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..2475699
--- /dev/null
+++ b/calendar/helpdocs/locale/it_IT/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Italian (Italy) (http://www.transifex.com/projects/p/kolab-documentation/language/it_IT/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it_IT\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..75070ed
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/index.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/index.po
new file mode 100644
index 0000000..f14d861
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..4f36144
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..e308c87
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..4c31008
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..5bc337a
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..763aeca
--- /dev/null
+++ b/calendar/helpdocs/locale/ja_JP/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Japanese (http://www.transifex.com/projects/p/kolab-documentation/language/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..7115cca
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/index.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/index.po
new file mode 100644
index 0000000..a82de84
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..c0d1994
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..3498b0a
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..75ff081
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..7d6784f
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..bc08353
--- /dev/null
+++ b/calendar/helpdocs/locale/ku_IQ/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Kurdish (Iraq) (http://www.transifex.com/projects/p/kolab-documentation/language/ku_IQ/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ku_IQ\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..686f8d8
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/index.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/index.po
new file mode 100644
index 0000000..aa36909
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..8a77ff1
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..fd10d2d
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..463ffc3
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..f3f5c09
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..f72af07
--- /dev/null
+++ b/calendar/helpdocs/locale/nl_NL/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/kolab-documentation/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..0ab70c9
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/index.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/index.po
new file mode 100644
index 0000000..f085bbb
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..2f81ca4
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..bda6ae9
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..7e991a2
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..34341d4
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..0fce804
--- /dev/null
+++ b/calendar/helpdocs/locale/pl_PL/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/kolab-documentation/language/pl_PL/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl_PL\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..d61edf3
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/index.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/index.po
new file mode 100644
index 0000000..2a5b733
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..7e5b748
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..dde4c33
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..65f6308
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..9d72b92
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..b0c1bd1
--- /dev/null
+++ b/calendar/helpdocs/locale/pt_BR/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/kolab-documentation/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..ac9bb6c
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/index.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/index.po
new file mode 100644
index 0000000..a0901d1
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..f37cec2
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..68a1687
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..3a09122
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..9c22a5d
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..68e6022
--- /dev/null
+++ b/calendar/helpdocs/locale/ro/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Romanian (http://www.transifex.com/projects/p/kolab-documentation/language/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..b4409dc
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/index.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/index.po
new file mode 100644
index 0000000..d1c01c9
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..c354273
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..af415a7
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..2aa699c
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..c0712dc
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..a3396c9
--- /dev/null
+++ b/calendar/helpdocs/locale/ru_RU/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Russian (Russia) (http://www.transifex.com/projects/p/kolab-documentation/language/ru_RU/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru_RU\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..3aa9d5c
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/index.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/index.po
new file mode 100644
index 0000000..e7f3cc0
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..992456e
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..a4196e7
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..dc5a219
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..38ff457
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..fcef687
--- /dev/null
+++ b/calendar/helpdocs/locale/sk/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/kolab-documentation/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..2917451
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/index.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/index.po
new file mode 100644
index 0000000..2a097f1
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..8437ff4
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..d12eda2
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..f9f28d9
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..05c394f
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..b8fdd00
--- /dev/null
+++ b/calendar/helpdocs/locale/sv/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/kolab-documentation/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..ee472d0
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/index.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/index.po
new file mode 100644
index 0000000..bc93d0e
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..f1ae89a
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..7033876
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..19527ea
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..629bc92
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..e547e2d
--- /dev/null
+++ b/calendar/helpdocs/locale/sv_SE/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Swedish (Sweden) (http://www.transifex.com/projects/p/kolab-documentation/language/sv_SE/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv_SE\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..bd0696f
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/index.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/index.po
new file mode 100644
index 0000000..b57872c
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..e5de332
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..8fc0022
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..95d168b
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..f478718
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..d7836d4
--- /dev/null
+++ b/calendar/helpdocs/locale/tr_TR/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Turkish (Turkey) (http://www.transifex.com/projects/p/kolab-documentation/language/tr_TR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr_TR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..33c7485
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/index.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/index.po
new file mode 100644
index 0000000..a6a1933
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..1597780
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..ca00e16
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..a8432bf
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..7e951db
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..a72f109
--- /dev/null
+++ b/calendar/helpdocs/locale/uk/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/kolab-documentation/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..725d2ee
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/index.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/index.po
new file mode 100644
index 0000000..b732529
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..b454886
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..1f39e04
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..f50b5f3
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..49374f5
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..dd7446b
--- /dev/null
+++ b/calendar/helpdocs/locale/vi/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (http://www.transifex.com/projects/p/kolab-documentation/language/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..ea45ff3
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/index.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/index.po
new file mode 100644
index 0000000..8d086b7
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..cb29571
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..a0acbeb
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..e7e34e3
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..ad72d9b
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..cd60cda
--- /dev/null
+++ b/calendar/helpdocs/locale/vi_VN/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/kolab-documentation/language/vi_VN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: vi_VN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..90241f7
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/index.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/index.po
new file mode 100644
index 0000000..0277b13
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..220abcc
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..f0e51c4
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..ee11b3b
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..bfa59d4
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..d457d42
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_CN/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/importexport.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/importexport.po
new file mode 100644
index 0000000..fb4f6d8
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/importexport.po
@@ -0,0 +1,96 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid ""
+"Event data is usually exchanged using the standard |iCal|_ format which is "
+"supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid ""
+"The calendar view will be refreshed to display the newly imported events. "
+"Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid ""
+"Events from your calendars can be exported and downloaded in the |iCal|_ "
+"format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid ""
+"With the *Events from* selector you choose the time constraints for "
+"exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid ""
+"Choose where to save the exported .ics file if prompted, otherwise check the"
+" \"Downloads\" folder on your computer."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/index.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/index.po
new file mode 100644
index 0000000..e57a4ba
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/index.po
@@ -0,0 +1,28 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid ""
+"The *Calendar* gives you access to your personal and shared calendar and "
+"scheduling functions."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/invitations.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/invitations.po
new file mode 100644
index 0000000..c36cf8a
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/invitations.po
@@ -0,0 +1,103 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid ""
+"In chapter :ref:`calendar-event-participants` we have learned how to invite "
+"other people to an event. This will send out invitation emails to all the "
+"participants with the event data attached. That allows one to directly "
+"accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid ""
+"When the webmail system opens an invitation email with event data attached, "
+"it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid ""
+"Right in the box shown above, you can accept or decline the invitation by "
+"clicking the according button. This will send an automated response to the "
+"event organizer informing her about your decision and letting her update "
+"your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid ""
+"In case you accept, by either clicking *Accept* or *Maybe*, this will also "
+"copy the event into your personal calendar. The selector right next to the "
+"buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid ""
+"The copy in your calendar now knows about the invitation and its original "
+"sender. If you now delete it from your calendar, you'll be asked whether "
+"this should send a declination message to the person who organizes the "
+"event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid ""
+"After acceping or declining, the email message containing the invitation can"
+" be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid ""
+"As an organizer who has invited others to an event, you'll receive responses"
+" to the automatically sent invitations when the attendees either accept or "
+"decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid ""
+"Such messages are also identified by the webmail system and again a yellow "
+"box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid ""
+"By clicking the *Update the participant's status* button, the original event"
+" in your calendar will be updated with the RSVP status from the person who "
+"responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid ""
+"When you now look at the event details in the calendar view, the status "
+"icons next to each participant now displays the new status."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/manage.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/manage.po
new file mode 100644
index 0000000..e489f34
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/manage.po
@@ -0,0 +1,342 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid ""
+"All functions to maintain your events are accessible from the main calendar "
+"view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid ""
+"Click the *New event* button in the toolbar to get an empty dialog where you"
+" enter the :ref:`event properties <calendar-edit-event>` such as summary, "
+"date/time, reminders, etc. Click *Save* to finally add it to the selected "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid ""
+"Navigate the calendar view to the date you want to add an event for. Then "
+"mark the range of time (or dates in month view) with the mouse by pressing "
+"the button at the time the event should start and releasing it again at time"
+" it finishes. This will open the :ref:`event dialog <calendar-edit-event>` "
+"with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid ""
+"In order to create new all-day events, double-click the desired day in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid ""
+"When clicking an event in the calendar view, a dialog showing its details is"
+" displayed. Clicking the *Edit* button in that dialog opens the form to edit"
+" all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid ""
+"The edit form is divided into different section which can be switched using "
+"the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid ""
+"This general section has text fields and selectors for various properties of"
+" an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid ""
+"``Summary``: The title of the event. This is what you will see in the "
+"calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid ""
+"``Reminder``: Will pop up with an notification at a the specified time "
+"before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid ""
+"``Calendar``: The calendar the event is saved in. Change it to move an event"
+" from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid ""
+"``Category``: The type of event. Categories can also be used for "
+":ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid ""
+"``Show me as``: The representation in your free/busy scheduling calendar "
+"visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid ""
+"``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing "
+"your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid ""
+"For periodically recurring event series, this tabs has the settings how an "
+"event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid ""
+"``Every``: How often the frequency will be relevant. For example, for an "
+"event that takes place every other week choose Weekly and then 2. If you "
+"choose a frequency of weekly or monthly you can select which days of the "
+"week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid ""
+"``Until``: Determines the duration of the repetition. The recurrence can "
+"either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid ""
+"An important part of managing your schedule is to invite others to events "
+"and track their RSVP. In this part of the edit dialog you can manage the "
+"participants of an event. Read more about this further down in the :ref"
+":`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid ""
+"Sometimes a description text isn't enough to collect information for a "
+"specific event. Switch to this tab to attach files to the current event or "
+"to remove them again. Adding files works pretty much the same as "
+":ref:`attaching them to email messages <mail-compose-attachments>`: first "
+"select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid ""
+"Don't forget to finally save the changes by clicking *Save* in the event "
+"edit dialog. Even switching back and forth the tabs will not yet save the "
+"data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid ""
+"If an existing event shall be rescheduled to another time or date, you'll "
+"find it handy to do that directly in the calendar view without opening the "
+"edit form. Simply grab the event block with the mouse and move it to the new"
+" date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid ""
+"In *Month* and *Day* view, the event blocks have a small handle at the "
+"bottom. Drag this with the mouse in order to resize the event meaning to "
+"adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid ""
+"While logged in to the webmail, event reminders will be displayed with pop-"
+"up boxes at the specified time before the event starts. You can specify if "
+"you want to see alarms for every calendar individually. Enable or disable "
+"reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the "
+":ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid ""
+"When a reminder box pops up, you can either dismiss the notification for all"
+" events or each one individually. When dismissed, no further reminders will "
+"be displayed. Choose a time from the *Snooze* menu to get another reminder "
+"after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid ""
+"If you need to set up a meeting, and keep track of who's attending and who "
+"is not, the calendar can do this as well as you to automatically send "
+"invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid ""
+"When creating a new event, switch to the *Participants* tab. You're already "
+"listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid ""
+"Enter the name or email address of the person to invite. Contacts from the "
+"address book are suggested as you type. In order to send invitations, make "
+"sure the entered contact has an email address. Type it in the form ``Person "
+"Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid ""
+"Check the *Send invitations* box if the application should send out "
+"invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid ""
+"Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid ""
+"Once all the participants are added to the list, you see the individual "
+"availability status for each one of them, given that this information is "
+"available. In case not everybody is free, click the *Find availability...* "
+"button to open the scheduling dialog. In that dialog, detailed availability "
+"information for all participants is displayed. Use the *Previous/Next Slot* "
+"buttons to find the next time slot where all required participants are "
+"available. Or drag the gray area representing the event duration with the "
+"mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid ""
+"Click *Select* to copy the rescheduled date/time back into the event form "
+"and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid ""
+"How to process incoming event invitations is described in chapter :ref"
+":`calendar-invitations`."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/overview.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/overview.po
new file mode 100644
index 0000000..fa0e4df
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/overview.po
@@ -0,0 +1,266 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:33+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid ""
+"The screen of the calendar module presents the following parts: the "
+":ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget "
+"<calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual "
+"toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid ""
+"The central part of the screen displays the schedule with events from the "
+"active calendars matching the current date range. The active date range is "
+"displayed above the calendar in the toolbar area and can be moved forward or"
+" backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid ""
+"You can view your calendar events in Day, Week, Month or Agenda view. Toggle"
+" the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid ""
+"All events of a single day appear at the time the begin and spawn a box "
+"until their end time. The time scale is displayed in the left side of the "
+"view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid ""
+"Similar to the day view but lists all days of the week horizontally. All-day"
+" events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid ""
+"Shows all events of the selected month at a time. Each event only appears as"
+" a single line and if there are more events in a day than can be listed, a "
+"number at the bottom of the day field indicates that. Click that link to "
+"open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid ""
+"The agenda view shows a list of events for the selected range in a "
+"chronological order and divided by headers denoting either days, weeks or "
+"months. Both the number of the days considered for the listing as well as "
+"the mode how to divide list can be adjusted with the controls at the bottom "
+"of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid ""
+"For all the views, the small calendar on the left highlights the currently "
+"listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid ""
+"Use the mini calendar widget on the left to jump to a specific date. Simply "
+"click a date and the date range of the current view moves to include the "
+"selected day. The left/right arrows in the mini calendar's header quickly "
+"cycle through the months. Use the drop-down menus hidden under the month and"
+" year display in the widget header to directly jump to another month or "
+"year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid ""
+"A shortcut to switch the calendar view back to today or the current week "
+"provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid ""
+"Click an event box in the calendar view to open a dialog displaying all "
+"details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid ""
+"The search box above the calendar view lets you quickly get a list of events"
+" matching the entered keyword in either the title, location, description or "
+"attendees. Enter the search term into the box and press <Enter> on your "
+"keyboard to start the search. The calendar view will switch to *Agenda* mode"
+" in order to display a list of matches. Of course you can switch the view "
+"again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid ""
+"Events are searched within a certain date reange only which is displayed "
+"above the calendar view. Use the mini calendar widget or the arrow toolbar "
+"buttons and the range selector below the agenda view to adjust the time "
+"frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid ""
+"For searching as well as for normal views, only events from active calendars"
+" are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or "
+"hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid ""
+"Reset the search by clicking the *Reset search* icon on the right border of "
+"the search box. This will also switch the calendar view to whatever mode you"
+" had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid ""
+"Events can be organized in different calendars which are all displayed in "
+"the lower left list. Use the checkboxes in that list to show or hide events "
+"from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid ""
+"Beside your personal calendars, the list also displays calendars shared by "
+"other users or ones that are shared amongst your workgroup. Small icons in "
+"the list give a hint about the origin and some of them are possibly read-"
+"only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid ""
+"In order to better distinguish the events from various calendars in the "
+"calendar view, calendars have a color assigned which is used to colorize the"
+" events on the screen. Check the :ref:`settings-calendar` for more advanced "
+"options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid ""
+"You can create any number of calendars to store all your events and name "
+"them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid ""
+"Click the gear icon in the calendars list footer and select *Edit* from the "
+"options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid ""
+"Click the gear icon in the calendars list footer and select *Remove* from "
+"the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid ""
+"After a confirmation dialog, the selected calendar with all its events will "
+"be deleted. Caution: This action cannot be undone!"
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/settings.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/settings.po
new file mode 100644
index 0000000..88a5073
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/settings.po
@@ -0,0 +1,178 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid ""
+"The settings for the calendar module are listed in *Settings > Preferences* "
+"and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid ""
+"Lets you select the :ref:`calendar-view` which is visible by default when "
+"opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid ""
+"How one hour in day and week view is divided vertically. If for example set "
+"to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid ""
+"When opening the day or week view, the listing of events starts at this "
+"time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid ""
+"This time range will be used in the :ref:`availability finder <calendar-"
+"availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid ""
+"The coloring of the title of an event block (\"outline\") as well as the "
+"background color of the box (\"content\") in day and week views is "
+"influenced by the color of the calendar an event belongs to and/or the color"
+" of the category it is assigned to. This setting lets you control which "
+"source for coloring to use or if you even want a combined coloring that "
+"reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid ""
+"When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid ""
+"This is the default selection for saving new events. Used in both the "
+"calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid ""
+"This block allows the management of categories used in your calendar and "
+"assign colors to them. Use the color picker to change the color by clicking "
+"on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid ""
+"To add a new category, enter its unique name into the text box below the "
+"listing and then click the *Add category** button to add it. Note that you "
+"still need to click the *Save* button at the bottom of the preferences panel"
+" in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid ""
+"The calendar view and also display birthdays from contacts saved in your "
+"address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid ""
+"Choose from which address books you'd like to see birthdays in your "
+"calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid ""
+"This option controls whether and when to display reminder notifications for "
+"upcoming birthdays."
+msgstr ""
diff --git a/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po
new file mode 100644
index 0000000..4f3c8d7
--- /dev/null
+++ b/calendar/helpdocs/locale/zh_TW/LC_MESSAGES/sharing.po
@@ -0,0 +1,99 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Kolab Documentation\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\n"
+"PO-Revision-Date: 2014-11-27 23:34+0000\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/kolab-documentation/language/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid ""
+"For collaboration, sharing calendars is an important feature. In the "
+":ref:`overview <calendar-lists>`, we have already learned how calendars "
+"others share with you appear in the calendars list. The following now "
+"explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid ""
+"Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-"
+"edit-properties>`. Double-click a calendar in the list on the left and then "
+"select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid ""
+"The table displays who already has permission to see and modify the selected"
+" calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid ""
+"Enter the username or choose one from the autocompletion menu that appears "
+"when you start typing. Instead of a specific user, permissons can be granted"
+" for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid ""
+"Double-click an entry to edit the permissions for a particular user or "
+"group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid ""
+"For removing existing permissions, select the according entry in the list "
+"and then choose *Delete* from the menu behind the gear icon in the footer of"
+" the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid ""
+"Calendars shared by others are not showing up right away in the list within "
+"the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to"
+" see all resources you can access. There's a shortcut to this: click *Manage"
+" folders* in the options menu behind the gear icon located the footer of the"
+" calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid ""
+"In order to make a shared calendar appear in the calendars list, locate it "
+"in the folder manager and check the *Subscribed* mark in the list. Only "
+"subscribed calendars are visible in the calendar view."
+msgstr ""
diff --git a/calendar/helpdocs/po/importexport.pot b/calendar/helpdocs/po/importexport.pot
new file mode 100644
index 0000000..24d5e01
--- /dev/null
+++ b/calendar/helpdocs/po/importexport.pot
@@ -0,0 +1,86 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/importexport.rst:9
+msgid "Import/Export"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:11
+msgid "Event data is usually exchanged using the standard |iCal|_ format which is supported for import and export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:16
+msgid "Importing Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:18
+msgid "This is how to add events from an |iCal|_ (.ics) file:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:20
+msgid "Click the *Import* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:21
+msgid "Then select the file to import from your computer's hard drive."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:22
+msgid "Select the calendar to import the events to."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:23
+msgid "Select the threshold for old events to be imported."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:24
+msgid "Click *Import* and wait for the upload to finish."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:26
+msgid "The calendar view will be refreshed to display the newly imported events. Verify that the according calendar is active if you don't see them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:34
+msgid "Exporting Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:36
+msgid "Events from your calendars can be exported and downloaded in the |iCal|_ format."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:38
+msgid "Click the *Export* toolbar button in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:39
+msgid "Select the calendar where events should be exported from."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:40
+msgid "With the *Events from* selector you choose the time constraints for exporting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:41
+msgid "Click the *Export* button to start the export."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/importexport.rst:42
+msgid "Choose where to save the exported .ics file if prompted, otherwise check the \"Downloads\" folder on your computer."
+msgstr ""
+
diff --git a/calendar/helpdocs/po/index.pot b/calendar/helpdocs/po/index.pot
new file mode 100644
index 0000000..3c26113
--- /dev/null
+++ b/calendar/helpdocs/po/index.pot
@@ -0,0 +1,26 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/index.rst:9
+msgid "Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/index.rst:11
+msgid "The *Calendar* gives you access to your personal and shared calendar and scheduling functions."
+msgstr ""
+
diff --git a/calendar/helpdocs/po/invitations.pot b/calendar/helpdocs/po/invitations.pot
new file mode 100644
index 0000000..272f979
--- /dev/null
+++ b/calendar/helpdocs/po/invitations.pot
@@ -0,0 +1,74 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/invitations.rst:8
+msgid "Handle Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:10
+msgid "In chapter :ref:`calendar-event-participants` we have learned how to invite other people to an event. This will send out invitation emails to all the participants with the event data attached. That allows one to directly accept or decline an event invitation."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:17
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:19
+msgid "When the webmail system opens an invitation email with event data attached, it'll display a yellow box in the preview pane or the email view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:25
+msgid "Accept/Decline Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:27
+msgid "Right in the box shown above, you can accept or decline the invitation by clicking the according button. This will send an automated response to the event organizer informing her about your decision and letting her update your participant status in her calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:31
+msgid "In case you accept, by either clicking *Accept* or *Maybe*, this will also copy the event into your personal calendar. The selector right next to the buttons lets you choose the right one."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:35
+msgid "The copy in your calendar now knows about the invitation and its original sender. If you now delete it from your calendar, you'll be asked whether this should send a declination message to the person who organizes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:39
+msgid "After acceping or declining, the email message containing the invitation can be deleted."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:43
+msgid "Process Invitation Replies"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:45
+msgid "As an organizer who has invited others to an event, you'll receive responses to the automatically sent invitations when the attendees either accept or decline them."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:48
+msgid "Such messages are also identified by the webmail system and again a yellow box appears in the message view:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:53
+msgid "By clicking the *Update the participant's status* button, the original event in your calendar will be updated with the RSVP status from the person who responded here."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/invitations.rst:56
+msgid "When you now look at the event details in the calendar view, the status icons next to each participant now displays the new status."
+msgstr ""
+
diff --git a/calendar/helpdocs/po/manage.pot b/calendar/helpdocs/po/manage.pot
new file mode 100644
index 0000000..a653c56
--- /dev/null
+++ b/calendar/helpdocs/po/manage.pot
@@ -0,0 +1,250 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/manage.rst:7
+msgid "Manage Your Schedule"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:9
+msgid "All functions to maintain your events are accessible from the main calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:13
+msgid "Add Events to a Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:18
+msgid "**Via toolbar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:16
+msgid "Click the *New event* button in the toolbar to get an empty dialog where you enter the :ref:`event properties <calendar-edit-event>` such as summary, date/time, reminders, etc. Click *Save* to finally add it to the selected calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:27
+msgid "**At a specific date/time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:21
+msgid "Navigate the calendar view to the date you want to add an event for. Then mark the range of time (or dates in month view) with the mouse by pressing the button at the time the event should start and releasing it again at time it finishes. This will open the :ref:`event dialog <calendar-edit-event>` with the selected date/time range already filled in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:26
+msgid "In order to create new all-day events, double-click the desired day in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:33
+msgid "Edit and Reschedule Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:36
+msgid "The Event Dialog"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:38
+msgid "When clicking an event in the calendar view, a dialog showing its details is displayed. Clicking the *Edit* button in that dialog opens the form to edit all properties of the selected event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:41
+msgid "The edit form is divided into different section which can be switched using the tabs on top of the dialog:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:44
+msgid "**Summary**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:46
+msgid "This general section has text fields and selectors for various properties of an event. Here's a description of all the possible values:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:49
+msgid "``Summary``: The title of the event. This is what you will see in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:50
+msgid "``Location``: Where the event will be taking place."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:51
+msgid "``Description``: Any text that describes the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:52
+msgid "``URL``: A link to more information about this event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:53
+msgid "``Start``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:54
+msgid "``End``: Date and time when the event starts."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:55
+msgid "``all-day``: Check this if the event has no start/end time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:56
+msgid "``Reminder``: Will pop up with an notification at a the specified time before the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:57
+msgid "``Calendar``: The calendar the event is saved in. Change it to move an event from one calendar to another."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:58
+msgid "``Category``: The type of event. Categories can also be used for :ref:`coloring <settings-calendar>`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:59
+msgid "``Show me as``: The representation in your free/busy scheduling calendar visible to others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:60
+msgid "``Priority``: The priority value of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:61
+msgid "``Privacy``: Flag an event as \"private\" or \"confidential\" when sharing your calendar with others."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:63
+msgid "**Recurrence**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:65
+msgid "For periodically recurring event series, this tabs has the settings how an event is repeated over time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:68
+msgid "``Repeat``: Start with selecting a repetition interval (e.g. monthly)"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:69
+msgid "``Every``: How often the frequency will be relevant. For example, for an event that takes place every other week choose Weekly and then 2. If you choose a frequency of weekly or monthly you can select which days of the week or month the event will occur."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:71
+msgid "``Until``: Determines the duration of the repetition. The recurrence can either run forever, for a number it times or until a specific date."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:73
+msgid "**Participants**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:75
+msgid "An important part of managing your schedule is to invite others to events and track their RSVP. In this part of the edit dialog you can manage the participants of an event. Read more about this further down in the :ref:`calendar-event-participants` section."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:79
+msgid "**Attachments**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:81
+msgid "Sometimes a description text isn't enough to collect information for a specific event. Switch to this tab to attach files to the current event or to remove them again. Adding files works pretty much the same as :ref:`attaching them to email messages <mail-compose-attachments>`: first select a file from your local disk and click *Upload* in order to attach it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:86
+msgid "Don't forget to finally save the changes by clicking *Save* in the event edit dialog. Even switching back and forth the tabs will not yet save the data."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:93
+msgid "Moving and Resizing with the Mouse"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:99
+msgid "If an existing event shall be rescheduled to another time or date, you'll find it handy to do that directly in the calendar view without opening the edit form. Simply grab the event block with the mouse and move it to the new date or time. Release the mouse button to complete."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:103
+msgid "In *Month* and *Day* view, the event blocks have a small handle at the bottom. Drag this with the mouse in order to resize the event meaning to adjust its duration."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:111
+msgid "Get Notifications"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:117
+msgid "While logged in to the webmail, event reminders will be displayed with pop-up boxes at the specified time before the event starts. You can specify if you want to see alarms for every calendar individually. Enable or disable reminders in :ref:`Calendar Settings <calendar-edit-properties>` from the :ref:`calendar-lists`."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:122
+msgid "Dismiss or Snooze Reminders"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:124
+msgid "When a reminder box pops up, you can either dismiss the notification for all events or each one individually. When dismissed, no further reminders will be displayed. Choose a time from the *Snooze* menu to get another reminder after the selected time."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:133
+msgid "Inviting Other People"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:135
+msgid "If you need to set up a meeting, and keep track of who's attending and who is not, the calendar can do this as well as you to automatically send invitations and read their responses."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:138
+msgid "When creating a new event, switch to the *Participants* tab. You're already listed as the organizer of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:142
+msgid "Enter the name or email address of the person to invite. Contacts from the address book are suggested as you type. In order to send invitations, make sure the entered contact has an email address. Type it in the form ``Person Name <email@address.com>``."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:145
+msgid "Click *Add participant* to add the person to the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:146
+msgid "Select a *Role* (e.g. required or optional) for this person."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:147
+msgid "Repeat 1-3 for further participants."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:148
+msgid "Check the *Send invitations* box if the application should send out invitation emails."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:150
+msgid "Invitations will be sent out when you click *Save* and the event is created."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:158
+msgid "Find Availability"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:160
+msgid "Once all the participants are added to the list, you see the individual availability status for each one of them, given that this information is available. In case not everybody is free, click the *Find availability...* button to open the scheduling dialog. In that dialog, detailed availability information for all participants is displayed. Use the *Previous/Next Slot* buttons to find the next time slot where all required participants are available. Or drag the gray area representing the event duration with the mouse to manually select a free slot."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:166
+msgid "Click *Select* to copy the rescheduled date/time back into the event form and to close this dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:170
+msgid "Receive Event Invitations"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/manage.rst:172
+msgid "How to process incoming event invitations is described in chapter :ref:`calendar-invitations`."
+msgstr ""
+
diff --git a/calendar/helpdocs/po/overview.pot b/calendar/helpdocs/po/overview.pot
new file mode 100644
index 0000000..80297d1
--- /dev/null
+++ b/calendar/helpdocs/po/overview.pot
@@ -0,0 +1,195 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/overview.rst:6
+msgid "Overview"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:8
+msgid "The screen of the calendar module presents the following parts: the :ref:`Calendar View <calendar-view>` itself, a small :ref:`Calendar Widget <calendar-minicalendar>` the :ref:`calendar-lists` as well as the usual toolbar and search box."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:17
+msgid "Calendar View"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:19
+msgid "The central part of the screen displays the schedule with events from the active calendars matching the current date range. The active date range is displayed above the calendar in the toolbar area and can be moved forward or backward in time using the arrow buttons right next to the title."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:27
+msgid "Change Views"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:29
+msgid "You can view your calendar events in Day, Week, Month or Agenda view. Toggle the view mode using the toolbar buttons above the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:34
+msgid "**Day**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:33
+msgid "All events of a single day appear at the time the begin and spawn a box until their end time. The time scale is displayed in the left side of the view. All-day events appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "**Week**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:37
+msgid "Similar to the day view but lists all days of the week horizontally. All-day events again appear at the top."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:42
+msgid "**Month**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:40
+msgid "Shows all events of the selected month at a time. Each event only appears as a single line and if there are more events in a day than can be listed, a number at the bottom of the day field indicates that. Click that link to open a zoomed view of that single day."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:47
+msgid "**Agenda**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:45
+msgid "The agenda view shows a list of events for the selected range in a chronological order and divided by headers denoting either days, weeks or months. Both the number of the days considered for the listing as well as the mode how to divide list can be adjusted with the controls at the bottom of the agenda view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:51
+msgid "For all the views, the small calendar on the left highlights the currently listed days."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:54
+msgid "Go to a specific Date"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:56
+msgid "Use the mini calendar widget on the left to jump to a specific date. Simply click a date and the date range of the current view moves to include the selected day. The left/right arrows in the mini calendar's header quickly cycle through the months. Use the drop-down menus hidden under the month and year display in the widget header to directly jump to another month or year."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:60
+msgid "A shortcut to switch the calendar view back to today or the current week provides the *Today* button located in the toolbar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:64
+msgid "Show Event Details"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:66
+msgid "Click an event box in the calendar view to open a dialog displaying all details of the event."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:70
+msgid "Searching Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:72
+msgid "The search box above the calendar view lets you quickly get a list of events matching the entered keyword in either the title, location, description or attendees. Enter the search term into the box and press <Enter> on your keyboard to start the search. The calendar view will switch to *Agenda* mode in order to display a list of matches. Of course you can switch the view again to display the search results differently."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:77
+msgid "Events are searched within a certain date reange only which is displayed above the calendar view. Use the mini calendar widget or the arrow toolbar buttons and the range selector below the agenda view to adjust the time frame to search in."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:81
+msgid "For searching as well as for normal views, only events from active calendars are displayed. Use the checkboxes in the :ref:`calendar-lists` to add or hide events from different calendars."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:84
+msgid "Reset the search by clicking the *Reset search* icon on the right border of the search box. This will also switch the calendar view to whatever mode you had before the search."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:93
+msgid "Calendars List"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:95
+msgid "Events can be organized in different calendars which are all displayed in the lower left list. Use the checkboxes in that list to show or hide events from the specific calendars in the main view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:100
+msgid "Beside your personal calendars, the list also displays calendars shared by other users or ones that are shared amongst your workgroup. Small icons in the list give a hint about the origin and some of them are possibly read-only which is denoted with a small lock icon."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:106
+msgid "Colorized Events"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:108
+msgid "In order to better distinguish the events from various calendars in the calendar view, calendars have a color assigned which is used to colorize the events on the screen. Check the :ref:`settings-calendar` for more advanced options how to colorize events in the calendar view."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:112
+msgid "You can create any number of calendars to store all your events and name them individually."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:116
+msgid "Create a New Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:118
+msgid "Click the + icon in the calendars list footer."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:119
+msgid "In the dialog, give the new calendar a unique name and assign a color."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:120
+msgid "Click *Save* to create it."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:122
+msgid "The calendar view will reload and list the new calendar on the left."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:127
+msgid "Edit Calendar Names and Settings"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:129
+#: ../../en_US/_plugins/calendar/overview.rst:137
+msgid "Select the calendar to edit by clicking it in the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:130
+msgid "Click the gear icon in the calendars list footer and select *Edit* from the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:131
+msgid "Adjust name, color or reminders settings in the edit dialog."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:132
+msgid "Click *Save* to finally update the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:135
+msgid "Remove entire Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:138
+msgid "Click the gear icon in the calendars list footer and select *Remove* from the options menu."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/overview.rst:139
+msgid "After a confirmation dialog, the selected calendar with all its events will be deleted. Caution: This action cannot be undone!"
+msgstr ""
+
diff --git a/calendar/helpdocs/po/settings.pot b/calendar/helpdocs/po/settings.pot
new file mode 100644
index 0000000..a10b38d
--- /dev/null
+++ b/calendar/helpdocs/po/settings.pot
@@ -0,0 +1,146 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/settings.rst:8
+msgid "Calendar Preferences"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:10
+msgid "The settings for the calendar module are listed in *Settings > Preferences* and are grouped by the following sections:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:14
+msgid "Main Options"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "**Default view**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:17
+msgid "Lets you select the :ref:`calendar-view` which is visible by default when opening the calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:21
+msgid "**Time slots per hour**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:20
+msgid "How one hour in day and week view is divided vertically. If for example set to 2, you will see events displayed in 30 minute blocks."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "**First weekday**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:24
+msgid "Which weekday to begin the week view with."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:28
+msgid "**First hour to show**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:27
+msgid "When opening the day or week view, the listing of events starts at this time. Of course all hours of a day are visible by scrolling further up."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "**Working hours**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:33
+msgid "This time range will be used in the :ref:`availability finder <calendar-availability-finder>` when automatically selecting free slots for a meeting."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:40
+msgid "**Event coloring**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:37
+msgid "The coloring of the title of an event block (\"outline\") as well as the background color of the box (\"content\") in day and week views is influenced by the color of the calendar an event belongs to and/or the color of the category it is assigned to. This setting lets you control which source for coloring to use or if you even want a combined coloring that reflects both, the assignment of calendars and categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "**Default reminder setting**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:43
+msgid "When creating new events, they'll have this type of reminder set by default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "**Default reminder time**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:46
+msgid "When enabling reminders in a new event, use this preset as default."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:51
+msgid "**Create new events in**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:49
+msgid "This is the default selection for saving new events. Used in both the calendar view and when accepting event invitations."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:54
+msgid "Categories"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:56
+msgid "This block allows the management of categories used in your calendar and assign colors to them. Use the color picker to change the color by clicking on the square color box in the categories list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:59
+msgid "To add a new category, enter its unique name into the text box below the listing and then click the *Add category** button to add it. Note that you still need to click the *Save* button at the bottom of the preferences panel in order to finally register the new categories."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:65
+msgid "Birthdays Calendar"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:67
+msgid "The calendar view and also display birthdays from contacts saved in your address book. This block controls how this is done."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "**Display birthdays calendar**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:71
+msgid "Enable the birthdays calendar feature with this checkbox."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "**From these address books**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:74
+msgid "Choose from which address books you'd like to see birthdays in your calendar."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:80
+msgid "**Show reminders**"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/settings.rst:77
+msgid "This option controls whether and when to display reminder notifications for upcoming birthdays."
+msgstr ""
+
diff --git a/calendar/helpdocs/po/sharing.pot b/calendar/helpdocs/po/sharing.pot
new file mode 100644
index 0000000..4bd8ab1
--- /dev/null
+++ b/calendar/helpdocs/po/sharing.pot
@@ -0,0 +1,74 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2014, roundcube.net
+# This file is distributed under the same license as the Roundcube Webmail Help package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Roundcube Webmail Help 1.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-11-26 23:28+0100\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"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: ../../en_US/_plugins/calendar/sharing.rst:11
+msgid "Sharing Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:13
+msgid "For collaboration, sharing calendars is an important feature. In the :ref:`overview <calendar-lists>`, we have already learned how calendars others share with you appear in the calendars list. The following now explains how to make personal calendars accessible to fellow users."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:19
+msgid "Share a Calendar with others"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:21
+msgid "Sharing is controlled through the :ref:`Calendar Settings Dialog <calendar-edit-properties>`. Double-click a calendar in the list on the left and then select the *Sharing* tab at the top of the dialog box:"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:27
+msgid "The table displays who already has permission to see and modify the selected calendar. In order to share the calendar with a new user do"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:30
+msgid "Click the *Add entry* button (+) in the table footer"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:31
+msgid "Enter the username or choose one from the autocompletion menu that appears when you start typing. Instead of a specific user, permissons can be granted for all users or guests."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:33
+msgid "Select the access rights you want to grant for the user"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:34
+msgid "Click *Save* to add the permission"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:36
+msgid "Double-click an entry to edit the permissions for a particular user or group."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:38
+msgid "For removing existing permissions, select the according entry in the list and then choose *Delete* from the menu behind the gear icon in the footer of the list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:43
+msgid "Subscribe to Shared Calendars"
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:45
+msgid "Calendars shared by others are not showing up right away in the list within the calendar view. Switch to :ref:`Settings > Folders <settings-folders>` to see all resources you can access. There's a shortcut to this: click *Manage folders* in the options menu behind the gear icon located the footer of the calendars list."
+msgstr ""
+
+#: ../../en_US/_plugins/calendar/sharing.rst:50
+msgid "In order to make a shared calendar appear in the calendars list, locate it in the folder manager and check the *Subscribed* mark in the list. Only subscribed calendars are visible in the calendar view."
+msgstr ""
+
diff --git a/calendar/lib/SabreDAV/.htaccess b/calendar/lib/SabreDAV/.htaccess
new file mode 100644
index 0000000..c0800be
--- /dev/null
+++ b/calendar/lib/SabreDAV/.htaccess
@@ -0,0 +1,2 @@
+# Deny all for risky include files of this __CENSORED__ quality plugin
+Require all denied
diff --git a/calendar/lib/SabreDAV/ChangeLog b/calendar/lib/SabreDAV/ChangeLog
new file mode 100644
index 0000000..bc03df9
--- /dev/null
+++ b/calendar/lib/SabreDAV/ChangeLog
@@ -0,0 +1,1111 @@
+1.8.6-stable (2013-06-18)
+ * The zip release ships with sabre/vobject 2.1.0.
+ * Includes changes from version 1.7.8.
+
+1.8.5-stable (2013-04-11)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Includes changes from version 1.7.7.
+
+1.8.4-stable (2013-04-08)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Includes changes from version 1.7.6.
+
+1.8.3-stable (2013-03-01)
+ * The zip release ships with sabre/vobject 2.0.6.
+ * Includes changes from version 1.7.5.
+ * Fixed: organizer email-address for shared calendars is now prefixed with
+ mailto:, as it should.
+
+1.8.2-stable (2013-01-19)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Includes changes from version 1.7.4.
+
+1.8.1-stable (2012-12-01)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Includes changes from version 1.7.3.
+ * Fixed: Typo in 1.7 migration script caused it to fail.
+
+1.8.0-stable (2012-11-08)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * BC Break: Moved the entire codebase to PHP namespaces.
+ * BC Break: Every backend package (CalDAV, CardDAV, Auth, Locks,
+ Principals) now has consistent naming conventions. There's a
+ BackendInterface, and an AbstractBackend class.
+ * BC Break: Changed a bunch of constructor signatures in the CalDAV
+ package, to reduce dependencies on the ACL package.
+ * BC Break: Sabre_CalDAV_ISharedCalendar now also has a getShares method,
+ so sharees can figure out who is also on a shared calendar.
+
+ * Added: Sabre_DAVACL_IPrincipalCollection interface, to advertise support
+ for principal-property-search on any node.
+ * Added: Simple console script to fire up a fileserver in the current
+ directory using PHP 5.4's built-in webserver.
+ * Added: Sharee's can now also read out the list of invites for a shared
+ calendar.
+ * Added: The Proxy principal classes now both implement an interface, for
+ greater flexiblity.
+
+1.7.8-stable (2013-06-17)
+ * The zip release ships with sabre/vobject 2.1.0.
+ * Changed: OldSabre\DAV\Client::verifyPeer is now a protected property
+ (instead of private).
+ * Fixed: Text was incorrectly escaped in the Href and HrefList properties,
+ disallowing urls with ampersands (&) in them.
+ * Added: deserializer for OldSabre\DAVACL\Property\CurrentUserPrivilegeSet.
+ * Fixed: Issue 335: Client only deserializes properties with status 200.
+ * Fixed: Issue 341: Escaping xml in 423 Locked error responses.
+ * Added: Issue 339: beforeGetPropertiesForPath event.
+
+1.7.7-stable (2013-04-11)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Fixed: Assets in the browser plugins were not being served on windows
+ machines.
+
+1.7.6-stable (2013-04-08)
+ * The zip release ships with sabre/vobject 2.0.7.
+ * Fixed: vcardurl in database schema can now hold 255 characters instead
+ of 80 (which is often way to small).
+ * Fixed: The browser plugin potentially allowed people to open any
+ arbitrary file on windows servers (CVE-2013-1939).
+
+1.7.5-stable (2013-03-01)
+ * The zip release ships with sabre/vobject 2.0.6.
+ * Change: No longer advertising support for 4.0 vcards. iOS and OS X
+ address book don't handle this well, and just advertising 3.0 support
+ seems like the most logical course of action.
+ * Added: ->setVerifyPeers to Sabre_DAV_Client (greatly resisting against
+ it, don't use this..).
+
+1.7.4-stable (2013-01-19)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Changed: To be compatibile with MS Office 2011 for Mac, a workaround was
+ removed that was added to support old versions of Windows XP (pre-SP3).
+ Indeed! We needed a crazy workaround to work with one MS product in the
+ past, and we can't keep that workaround to be compatible with another MS
+ product.
+ * Fixed: expand-properties REPORT had incorrect values for the href
+ element.
+ * Fixed: Range requests now work for non-seekable streams. (Thanks Alfred
+ Klomp).
+ * Fixed: Changed serialization of {DAV:}getlastmodified and
+ {DAV:}supportedlock to improve compatiblity with MS Office 2011 for Mac.
+ * Changed: reverted the automatic translation of 'DAV:' xml namespaces to
+ 'urn:DAV' when parsing files. Issues were reported with libxml 2.6.32,
+ on a relatively recent debian release, so we'll wait till 2015 to take
+ this one out again.
+ * Added: Sabre_DAV_Exception_ServiceUnavailable, for emitting 503's.
+
+1.7.3-stable (2012-12-01)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Fixed: Removing double slashes from getPropertiesForPath.
+ * Change: Marked a few more properties in the CardDAV as protected,
+ instead of private.
+ * Fixed: SharingPlugin now plays nicer with other plugins with similar
+ functionality.
+ * Fixed: Issue 174. Sending back HTTP/1.0 for requests with this version.
+
+1.7.2-stable (2012-11-08)
+ * The zip release ships with sabre/vobject 2.0.5.
+ * Added: ACL plugin advertises support for 'calendarserver-principal-
+ property-search'.
+ * Fixed: [#153] Allowing for relative http principals in iMip requests.
+ * Added: Support for cs:first-name and cs:last-name properties in sharing
+ invites.
+ * Fixed: Made a bunch of properties protected, where they were private
+ before.
+ * Added: Some non-standard properties for sharing to improve
+ compatibility.
+ * Fixed: some bugfixes in postgres sql script.
+ * Fixed: When requesting some properties using PROPFIND, they could show
+ up as both '200 Ok' and '403 Forbidden'.
+ * Fixed: calendar-proxy principals were not checked for deeper principal
+ membership than 1 level.
+ * Fixed: setGroupMemberSet argument now correctly receives relative
+ principal urls, instead of the absolute ones.
+ * Fixed: Server class will filter out any bonus properties if any extra
+ were returned. This means the implementor of the IProperty class can be
+ a bit lazier when implementing.
+
+Note: bug numbers after this line refer to Google Code tickets. We're using
+github now.
+
+1.7.1-stable (2012-10-07)
+ * Fixed: include path problem in the migration script.
+
+1.7.0-stable (2012-10-06)
+ * BC Break: The calendarobjects database table has a bunch of new
+ fields, and a migration script is required to ensure everything will
+ keep working. Read the wiki for more details.
+ * BC Break: The ICalendar interface now has a new method: calendarQuery.
+ * BC Break: In this version a number of classes have been deleted, that
+ have been previously deprecated. Namely:
+ - Sabre_DAV_Directory (now: Sabre_DAV_Collection)
+ - Sabre_DAV_SimpleDirectory (now: Sabre_DAV_SimpleCollection)
+ * BC Break: Sabre_CalDAV_Schedule_IMip::sendMessage now has an extra
+ argument. If you extended this class, you should fix this method. It's
+ only used for informational purposes.
+ * BC Break: The DAV: namespace is no longer converted to urn:DAV. This was
+ a workaround for a bug in older PHP versions (pre-5.3).
+ * Removed: Sabre.includes.php was deprecated, and is now removed.
+ * Removed: Sabre_CalDAV_Server was deprecated, and is now removed. Please
+ use Sabre_DAV_Server and check the examples in the examples/ directory.
+ * Changed: The Sabre_VObject library now spawned into it's own project!
+ The VObject library is still included in the SabreDAV zip package.
+ * Added: Experimental interfaces to allow implementation of caldav-sharing.
+ Note that no implementation is provided yet, just the api hooks.
+ * Added: Free-busy reporting compliant with the caldav-scheduling
+ standard. This allows iCal and other clients to fetch other users'
+ free-busy data.
+ * Added: Experimental NotificationSupport interface to add
+ caldav notifications.
+ * Added: VCF Export plugin. If enabled, it can generate an export of an
+ entire addressbook.
+ * Added: Support for PATCH using a SabreDAV format, to live-patch files.
+ * Added: Support for Prefer: return-minimal and Brief: t headers for
+ PROPFIND and PROPPATCH requests.
+ * Changed: Responsibility for dealing with the calendar-query is now
+ moved from the CalDAV plugin to the CalDAV backends. This allows for
+ heavy optimizations.
+ * Changed: The CalDAV PDO backend is now a lot faster for common
+ calendar queries.
+ * Changed: We are now using the composer autoloader.
+ * Changed: The CalDAV backend now all implement an interface.
+ * Changed: Instead of Sabre_DAV_Property, Sabre_DAV_PropertyInterface is
+ now the basis of every property class.
+ * Update: Caching results for principal lookups. This should cut down
+ queries and performance for a number of heavy requests.
+ * Update: ObjectTree caches lookups much more aggresively, which will help
+ especially speeding up a bunch of REPORT queries.
+ * Added: Support for the schedule-calendar-transp property.
+ * Fixed: Marking both the text/calendar and text/x-vcard as UTF-8
+ encoded.
+ * Fixed: Workaround for the SOGO connector, as it doesn't understand
+ receiving "text/x-vcard; charset=utf-8" for a contenttype.
+ * Added: Sabre_DAV_Client now throws more specific exceptions in cases
+ where we already has an exception class.
+ * Added: Sabre_DAV_PartialUpdate. This plugin allows you to use the
+ PATCH method to update parts of a file.
+ * Added: Tons of timezone name mappings for Microsoft Exchange.
+ * Added: Support for an 'exception' event in the server class.
+ * Fixed: Uploaded VCards without a UID are now rejected. (thanks Dominik!)
+ * Fixed: Rejecting calendar objects if they are not in the
+ supported-calendar-component list. (thanks Armin!)
+ * Fixed: Issue 219: serialize() now reorders correctly.
+ * Fixed: Sabre_DAV_XMLUtil no longer returns empty $dom->childNodes
+ if there is whitespace in $dom.
+ * Fixed: Returning 409 Conflict instead of 500 when an attempt is made to
+ create a file as a child of something that's not a collection.
+ * Fixed: Issue 237: xml-encoding values in SabreDAV error responses.
+ * Fixed: Returning 403, instead of 501 when an unknown REPORT is
+ requested.
+ * Fixed: Postfixing slash on {DAV:}owner properties.
+ * Fixed: Several embarrassing spelling mistakes in docblocks.
+
+1.6.10-stable (2013-06-17)
+ * Fixed: Text was incorrectly escaped in the Href and HrefList properties,
+ disallowing urls with ampersands (&) in them.
+ * Fixed: Issue 341: Escaping xml in 423 Locked error responses.
+
+1.6.9-stable (2013-04-11)
+ * Fixed: Assets in the browser plugins were not being served on windows
+ machines.
+
+1.6.8-stable (2013-04-08)
+ * Fixed: vcardurl in database schema can now hold 255 characters instead
+ of 80 (which is often way to small).
+ * Fixed: The browser plugin potentially allowed people to open any
+ arbitrary file on windows servers. (CVE-2013-1939).
+
+1.6.7-stable (2013-03-01)
+ * Change: No longer advertising support for 4.0 vcards. iOS and OS X
+ address book don't handle this well, and just advertising 3.0 support
+ seems like the most logical course of action.
+ * Added: ->setVerifyPeers to Sabre_DAV_Client (greatly resisting against
+ it, don't use this..).
+
+1.6.6-stable (2013-01-19)
+ * Fixed: Backported a fix for broken XML serialization in error responses.
+ (Thanks @DeepDiver1975!)
+
+1.6.5-stable (2012-10-04)
+ * Fixed: Workaround for line-ending bug OS X 10.8 addressbook has.
+ * Added: Ability to allow users to set SSL certificates for the Client
+ class. (Thanks schiesbn!).
+ * Fixed: Directory indexes with lots of nodes should be a lot faster.
+ * Fixed: Issue 235: E_NOTICE thrown when doing a propfind request with
+ Sabre_DAV_Client, and no valid properties are returned.
+ * Fixed: Issue with filtering on alarms in tasks.
+
+1.6.4-stable (2012-08-02)
+ * Fixed: Issue 220: Calendar-query filters may fail when filtering on
+ alarms, if an overridden event has it's alarm removed.
+ * Fixed: Compatibility for OS/X 10.8 iCal in the IMipHandler.
+ * Fixed: Issue 222: beforeWriteContent shouldn't be called for lock
+ requests.
+ * Fixed: Problem with POST requests to the outbox if mailto: was not lower
+ cased.
+ * Fixed: Yearly recurrence rule expansion on leap-days no behaves
+ correctly.
+ * Fixed: Correctly checking if recurring, all-day events with no dtstart
+ fall in a timerange if the start of the time-range exceeds the start of
+ the instance of an event, but not the end.
+ * Fixed: All-day recurring events wouldn't match if an occurence ended
+ exactly on the start of a time-range.
+ * Fixed: HTTP basic auth did not correctly deal with passwords containing
+ colons on some servers.
+ * Fixed: Issue 228: DTEND is now non-inclusive for all-day events in the
+ calendar-query REPORT and free-busy calculations.
+
+1.6.3-stable (2012-06-12)
+ * Added: It's now possible to specify in Sabre_DAV_Client which type of
+ authentication is to be used.
+ * Fixed: Issue 206: Sabre_DAV_Client PUT requests are fixed.
+ * Fixed: Issue 205: Parsing an iCalendar 0-second date interval.
+ * Fixed: Issue 112: Stronger validation of iCalendar objects. Now making
+ sure every iCalendar object only contains 1 component, and disallowing
+ vcards, forcing every component to have a UID.
+ * Fixed: Basic validation for vcards in the CardDAV plugin.
+ * Fixed: Issue 213: Workaround for an Evolution bug, that prevented it
+ from updating events.
+ * Fixed: Issue 211: A time-limit query on a non-relative alarm trigger in
+ a recurring event could result in an endless loop.
+ * Fixed: All uri fields are now a maximum of 200 characters. The Bynari
+ outlook plugin used much longer strings so this should improve
+ compatibility.
+ * Fixed: Added a workaround for a bug in KDE 4.8.2 contact syncing. See
+ https://bugs.kde.org/show_bug.cgi?id=300047
+ * Fixed: Issue 217: Sabre_DAV_Tree_FileSystem was pretty broken.
+
+1.6.2-stable (2012-04-16)
+ * Fixed: Sabre_VObject_Node::$parent should have been public.
+ * Fixed: Recurrence rules of events are now taken into consideration when
+ doing time-range queries on alarms.
+ * Fixed: Added a workaround for the fact that php's DateInterval cannot
+ parse weeks and days at the same time.
+ * Added: Sabre_DAV_Server::$exposeVersion, allowing you to hide SabreDAV's
+ version number from various outputs.
+ * Fixed: DTSTART values would be incorrect when expanding events.
+ * Fixed: DTSTART and DTEND would be incorrect for expansion of WEEKLY
+ BYDAY recurrences.
+ * Fixed: Issue 203: A problem with overridden events hitting the exact
+ date and time of a subsequent event in the recurrence set.
+ * Fixed: There was a problem with recurrence rules, for example the 5th
+ tuesday of the month, if this day did not exist.
+ * Added: New HTTP status codes from draft-nottingham-http-new-status-04.
+
+1.6.1-stable (2012-03-05)
+ * Added: createFile and put() can now return an ETag.
+ * Added: Sending back an ETag on for operations on CardDAV backends. This
+ should help with OS X 10.6 Addressbook compatibility.
+ * Fixed: Fixed a bug where an infinite loop could occur in the recurrence
+ iterator if the recurrence was YEARLY, with a BYMONTH rule, and either
+ BYDAY or BYMONTHDAY match the first day of the month.
+ * Fixed: Events that are excluded using EXDATE are still counted in the
+ COUNT= parameter in the RRULE property.
+ * Added: Support for time-range filters on VALARM components.
+ * Fixed: Correctly filtering all-day events.
+ * Fixed: Sending back correct mimetypes from the browser plugin (thanks
+ Jürgen).
+ * Fixed: Issue 195: Sabre_CardDAV pear package had an incorrect dependency.
+ * Fixed: Calendardata would be destroyed when performing a MOVE request.
+
+1.6.0-stable (2012-02-22)
+ * BC Break: Now requires PHP 5.3
+ * BC Break: Any node that implemented Sabre_DAVACL_IACL must now also
+ implement the getSupportedPrivilegeSet method. See website for details.
+ * BC Break: Moved functions from Sabre_CalDAV_XMLUtil to
+ Sabre_VObject_DateTimeParser.
+ * BC Break: The Sabre_DAVACL_IPrincipalCollection now has two new methods:
+ 'searchPrincipals' and 'updatePrincipal'.
+ * BC Break: Sabre_DAV_ILockable is removed and all related per-node
+ locking functionality.
+ * BC Break: Sabre_DAV_Exception_FileNotFound is now deprecated in favor of
+ Sabre_DAV_Exception_NotFound. The former will be removed in a later
+ version.
+ * BC Break: Removed Sabre_CalDAV_ICalendarUtil, use Sabre_VObject instead.
+ * BC Break: Sabre_CalDAV_Server is now deprecated, check out the
+ documentation on how to setup a caldav server with just
+ Sabre_DAV_Server.
+ * BC Break: Default Principals PDO backend now needs a new field in the
+ 'principals' table. See the website for details.
+ * Added: Ability to create new calendars and addressbooks from within the
+ browser plugin.
+ * Added: Browser plugin: icons for various nodes.
+ * Added: Support for FREEBUSY reports!
+ * Added: Support for creating principals with admin-level privileges.
+ * Added: Possibility to let server send out invitation emails on behalf of
+ CalDAV client, using Sabre_CalDAV_Schedule_IMip.
+ * Changed: beforeCreateFile event now passes data argument by reference.
+ * Changed: The 'propertyMap' property from Sabre_VObject_Reader, must now
+ be specified in Sabre_VObject_Property::$classMap.
+ * Added: Ability for plugins to tell the ACL plugin which principal
+ plugins are searchable.
+ * Added: [DAVACL] Per-node overriding of supported privileges. This allows
+ for custom privileges where needed.
+ * Added: [DAVACL] Public 'principalSearch' method on the DAVACL plugin,
+ which allows for easy searching for principals, based on their
+ properties.
+ * Added: Sabre_VObject_Component::getComponents() to return a list of only
+ components and not properties.
+ * Added: An includes.php file in every sub-package (CalDAV, CardDAV, DAV,
+ DAVACL, HTTP, VObject) as an alternative to the autoloader. This often
+ works much faster.
+ * Added: Support for the 'Me card', which allows Addressbook.app users
+ specify which vcard is their own.
+ * Added: Support for updating principal properties in the DAVACL principal
+ backends.
+ * Changed: Major refactoring in the calendar-query REPORT code. Should
+ make things more flexible and correct.
+ * Changed: The calendar-proxy-[read|write] principals will now only appear
+ in the tree, if they actually exist in the Principal backend. This should
+ reduce some problems people have been having with this.
+ * Changed: Sabre_VObject_Element_* classes are now renamed to
+ Sabre_VObject_Property. Old classes are retained for backwards
+ compatibility, but this will be removed in the future.
+ * Added: Sabre_VObject_FreeBusyGenerator to generate free-busy reports
+ based on lists of events.
+ * Added: Sabre_VObject_RecurrenceIterator to find all the dates and times
+ for recurring events.
+ * Fixed: Issue 97: Correctly handling RRULE for the calendar-query REPORT.
+ * Fixed: Issue 154: Encoding of VObject parameters with no value was
+ incorrect.
+ * Added: Support for {DAV:}acl-restrictions property from RFC3744.
+ * Added: The contentlength for calendar objects can now be supplied by a
+ CalDAV backend, allowing for more optimizations.
+ * Fixed: Much faster implementation of Sabre_DAV_URLUtil::encodePath.
+ * Fixed: {DAV:}getcontentlength may now be not specified.
+ * Fixed: Issue 66: Using rawurldecode instead of urldecode to decode paths
+ from clients. This means that + will now be treated as a literal rather
+ than a space, and this should improve compatibility with the Windows
+ built-in client.
+ * Added: Sabre_DAV_Exception_PaymentRequired exception, to emit HTTP 402
+ status codes.
+ * Added: Some mysql unique constraints to example files.
+ * Fixed: Correctly formatting HTTP dates.
+ * Fixed: Issue 94: Sending back Last-Modified header for 304 responses.
+ * Added: Sabre_VObject_Component_VEvent, Sabre_VObject_Component_VJournal,
+ Sabre_VObject_Component_VTodo and Sabre_VObject_Component_VCalendar.
+ * Changed: Properties are now also automatically mapped to their
+ appropriate classes, if they are created using the add() or __set()
+ methods.
+ * Changed: Cloning VObject objects now clones the entire tree, rather than
+ just the default shallow copy.
+ * Added: Support for recurrence expansion in the CALDAV:calendar-multiget
+ and CALDAV:calendar-query REPORTS.
+ * Changed: CalDAV PDO backend now sorts calendars based on the internal
+ 'calendarorder' field.
+ * Added: Issue 181: Carddav backends may no optionally not supply the carddata in
+ getCards, if etag and size are specified. This may speed up certain
+ requests.
+ * Added: More arguments to beforeWriteContent and beforeCreateFile (see
+ WritingPlugins wiki document).
+ * Added: Hook for iCalendar validation. This allows us to validate
+ iCalendar objects when they're uploaded. At the moment we're just
+ validating syntax.
+ * Added: VObject now support Windows Timezone names correctly (thanks
+ mrpace2).
+ * Added: If a timezonename could not be detected, we fall back on the
+ default PHP timezone.
+ * Added: Now a Composer package (thanks willdurand).
+ * Fixed: Support for \N as a newline character in the VObject reader.
+ * Added: afterWriteContent, afterCreateFile and afterUnbind events.
+ * Added: Postgresql example files. Not part of the unittests though, so
+ use at your own risk.
+ * Fixed: Issue 182: Removed backticks from sql queries, so it will work
+ with Postgres.
+
+1.5.9-stable (2012-04-16)
+ * Fixed: Issue with parsing timezone identifiers that were surrounded by
+ quotes. (Fixes emClient compatibility).
+
+1.5.8-stable (2012-02-22)
+ * Fixed: Issue 95: Another timezone parsing issue, this time in
+ calendar-query.
+
+1.5.7-stable (2012-02-19)
+ * Fixed: VObject properties are now always encoded before components.
+ * Fixed: Sabre_DAVACL had issues with multiple levels of privilege
+ aggregration.
+ * Changed: Added 'GuessContentType' plugin to fileserver.php example.
+ * Fixed: The Browser plugin will now trigger the correct events when
+ creating files.
+ * Fixed: The ICSExportPlugin now considers ACL's.
+ * Added: Made it optional to supply carddata from an Addressbook backend
+ when requesting getCards. This can make some operations much faster, and
+ could result in much lower memory use.
+ * Fixed: Issue 187: Sabre_DAV_UUIDUtil was missing from includes file.
+ * Fixed: Issue 191: beforeUnlock was triggered twice.
+
+1.5.6-stable (2012-01-07)
+ * Fixed: Issue 174: VObject could break UTF-8 characters.
+ * Fixed: pear package installation issues.
+
+1.5.5-stable (2011-12-16)
+ * Fixed: CalDAV time-range filter workaround for recurring events.
+ * Fixed: Bug in Sabre_DAV_Locks_Backend_File that didn't allow multiple
+ files to be locked at the same time.
+
+1.5.4-stable (2011-10-28)
+ * Fixed: GuessContentType plugin now supports mixed case file extensions.
+ * Fixed: DATE-TIME encoding was wrong in VObject. (we used 'DATETIME').
+ * Changed: Sending back HTTP 204 after a PUT request on an existing resource
+ instead of HTTP 200. This should fix Evolution CardDAV client
+ compatibility.
+ * Fixed: Issue 95: Parsing X-LIC-LOCATION if it's available.
+ * Added: All VObject elements now have a reference to their parent node.
+
+1.5.3-stable (2011-09-28)
+ * Fixed: Sabre_DAV_Collection was missing from the includes file.
+ * Fixed: Issue 152. iOS 1.4.2 apparantly requires HTTP/1.1 200 OK to be in
+ uppercase.
+ * Fixed: Issue 153: Support for files with mixed newline styles in
+ Sabre_VObject.
+ * Fixed: Issue 159: Automatically converting any vcard and icalendardata
+ to UTF-8.
+ * Added: Sabre_DAV_SimpleFile class for easy static file creation.
+ * Added: Issue 158: Support for the CARDDAV:supported-address-data
+ property.
+
+1.5.2-stable (2011-09-21)
+ * Fixed: carddata and calendardata MySQL fields are now of type
+ 'mediumblob'. 'TEXT' was too small sometimes to hold all the data.
+ * Fixed: {DAV:}supported-report-set is now correctly reporting the reports
+ for IAddressBook.
+ * Added: Sabre_VObject_Property::add() to add duplicate parameters to
+ properties.
+ * Added: Issue 151: Sabre_CalDAV_ICalendar and Sabre_CalDAV_ICalendarObject
+ interfaces.
+ * Fixed: Issue 140: Not returning 201 Created if an event cancelled the
+ creation of a file.
+ * Fixed: Issue 150: Faster URLUtil::encodePath() implementation.
+ * Fixed: Issue 144: Browser plugin could interfere with
+ TemporaryFileFilterPlugin if it was loaded first.
+ * Added: It's not possible to specify more 'alternate uris' in principal
+ backends.
+
+1.5.1-stable (2011-08-24)
+ * Fixed: Issue 137. Hiding action interface in HTML browser for
+ non-collections.
+ * Fixed: addressbook-query is now correctly returned from the
+ {DAV:}supported-report-set property.
+ * Fixed: Issue 142: Bugs in groupwareserver.php example.
+ * Fixed: Issue 139: Rejecting PUT requests with Content-Range.
+
+1.5.0-stable (2011-08-12)
+ * Added: CardDAV support.
+ * Added: An experimental WebDAV client.
+ * Added: MIME-Directory grouping support in the VObject library. This is
+ very useful for people attempting to parse vcards.
+ * BC Break: Adding parameters with the VObject libraries now overwrites
+ the previous parameter, rather than just add it. This makes more sense
+ for 99% of the cases.
+ * BC Break: lib/Sabre.autoload.php is now removed in favor of
+ lib/Sabre/autoload.php.
+ * Deprecated: Sabre_DAV_Directory is now deprecated and will be removed in
+ a future version. Use Sabre_DAV_Collection instead.
+ * Deprecated: Sabre_DAV_SimpleDirectory is now deprecated and will be
+ removed in a future version. Use Sabre_DAV_SimpleCollection instead.
+ * Fixed: Problem with overriding tablenames for the CalDAV backend.
+ * Added: Clark-notation parser to XML utility.
+ * Added: unset() support to VObject components.
+ * Fixed: Refactored CalDAV property fetching to be faster and simpler.
+ * Added: Central string-matcher for CalDAV and CardDAV plugins.
+ * Added: i;unicode-casemap support
+ * Fixed: VObject bug: wouldn't parse parameters if they weren't specified
+ in uppercase.
+ * Fixed: VObject bug: Parameters now behave more like Properties.
+ * Fixed: VObject bug: Parameters with no value are now correctly parsed.
+ * Changed: If calendars don't specify which components they allow, 'all'
+ components are assumed (e.g.: VEVENT, VTODO, VJOURNAL).
+ * Changed: Browser plugin now uses POST variable 'sabreAction' instead of
+ 'action' to reduce the chance of collisions.
+
+1.4.4-stable (2011-07-07)
+ * Fixed: Issue 131: Custom CalDAV backends could break in certain cases.
+ * Added: The option to override the default tablename all PDO backends
+ use. (Issue 60).
+ * Fixed: Issue 124: 'File' authentication backend now takes realm into
+ consideration.
+ * Fixed: Sabre_DAV_Property_HrefList now properly deserializes. This
+ allows users to update the {DAV:}group-member-set property.
+ * Added: Helper functions for DateTime-values in Sabre_VObject package.
+ * Added: VObject library can now automatically map iCalendar properties to
+ custom classes.
+
+1.4.3-stable (2011-04-25)
+ * Fixed: Issue 123: Added workaround for Windows 7 UNLOCK bug.
+ * Fixed: datatype of lastmodified field in mysql.calendars.sql. Please
+ change the DATETIME field to an INT to ensure this field will work
+ correctly.
+ * Change: Sabre_DAV_Property_Principal is now renamed to
+ Sabre_DAVACL_Property_Principal.
+ * Added: API level support for ACL HTTP method.
+ * Fixed: Bug in serializing {DAV:}acl property.
+ * Added: deserializer for {DAV:}resourcetype property.
+ * Added: deserializer for {DAV:}acl property.
+ * Added: deserializer for {DAV:}principal property.
+
+1.4.2-beta (2011-04-01)
+ * Added: It's not possible to disable listing of nodes that are denied
+ read access by ACL.
+ * Fixed: Changed a few properties in CalDAV classes from private to
+ protected.
+ * Fixed: Issue 119: Terrible things could happen when relying on
+ guessBaseUri, the server was running on the root of the domain and a user
+ tried to access a file ending in .php. This is a slight BC break.
+ * Fixed: Issue 118: Lock tokens in If headers without a uri should be
+ treated as the request uri, not 'all relevant uri's.
+ * Fixed: Issue 120: PDO backend was incorrectly fetching too much locks in
+ cases where there were similar named locked files in a directory.
+
+1.4.1-beta (2011-02-26)
+ * Fixed: Sabre_DAV_Locks_Backend_PDO returned too many locks.
+ * Fixed: Sabre_HTTP_Request::getHeader didn't return Content-Type when
+ running on apache, so a few workarounds were added.
+ * Change: Slightly changed CalDAV Backend API's, to allow for heavy
+ optimizations. This is non-bc breaking.
+
+1.4.0-beta (2011-02-12)
+ * Added: Partly RFC3744 ACL support.
+ * Added: Calendar-delegation (caldav-proxy) support.
+ * BC break: In order to fix Issue 99, a new argument had to be added to
+ Sabre_DAV_Locks_Backend_*::getLocks classes. Consult the classes for
+ details.
+ * Deprecated: Sabre_DAV_Locks_Backend_FS is now deprecated and will be
+ removed in a later version. Use PDO or the new File class instead.
+ * Deprecated: The Sabre_CalDAV_ICalendarUtil class is now marked
+ deprecated, and will be removed in a future version. Please use
+ Sabre_VObject instead.
+ * Removed: All principal-related functionality has been removed from the
+ Sabre_DAV_Auth_Plugin, and moved to the Sabre_DAVACL_Plugin.
+ * Added: VObject library, for easy vcard/icalendar parsing using a natural
+ interface.
+ * Added: Ability to automatically generate full .ics feeds off calendars.
+ To use: Add the Sabre_CalDAV_ICSExportPlugin, and add ?export to your
+ calendar url.
+ * Added: Plugins can now specify a pluginname, for easy access using
+ Sabre_DAV_Server::getPlugin().
+ * Added: beforeGetProperties event.
+ * Added: updateProperties event.
+ * Added: Principal listings and calendar-access can now be done privately,
+ disallowing users from accessing or modifying other users' data.
+ * Added: You can now pass arrays to the Sabre_DAV_Server constructor. If
+ it's an array with node-objects, a Root collection will automatically be
+ created, and the nodes are used as top-level children.
+ * Added: The principal base uri is now customizable. It used to be
+ hardcoded to 'principals/[user]'.
+ * Added: getSupportedReportSet method in ServerPlugin class. This allows
+ you to easily specify which reports you're implementing.
+ * Added: A '..' link to the HTML browser.
+ * Fixed: Issue 99: Locks on child elements were ignored when their parent
+ nodes were deleted.
+ * Fixed: Issue 90: lockdiscovery property and LOCK response now include a
+ {DAV}lockroot element.
+ * Fixed: Issue 96: support for 'default' collation in CalDAV text-match
+ filters.
+ * Fixed: Issue 102: Ensuring that copy and move with identical source and
+ destination uri's fails.
+ * Fixed: Issue 105: Supporting MKCALENDAR with no body.
+ * Fixed: Issue 109: Small fixes in Sabre_HTTP_Util.
+ * Fixed: Issue 111: Properly catching the ownername in a lock (if it's a
+ string)
+ * Fixed: Sabre_DAV_ObjectTree::nodeExist always returned false for the
+ root node.
+ * Added: Global way to easily supply new resourcetypes for certain node
+ classes.
+ * Fixed: Issue 59: Allowing the user to override the authentication realm
+ in Sabre_CalDAV_Server.
+ * Update: Issue 97: Looser time-range checking if there's a recurrence
+ rule in an event. This fixes 'missing recurring events'.
+
+1.3.0 (2010-10-14)
+ * Added: childExists method to Sabre_DAV_ICollection. This is an api
+ break, so if you implement Sabre_DAV_ICollection directly, add the method.
+ * Changed: Almost all HTTP method implementations now take a uri argument,
+ including events. This allows for internal rerouting of certain calls.
+ If you have custom plugins, make sure they use this argument. If they
+ don't, they will likely still work, but it might get in the way of
+ future changes.
+ * Changed: All getETag methods MUST now surround the etag with
+ double-quotes. This was a mistake made in all previous SabreDAV
+ versions. If you don't do this, any If-Match, If-None-Match and If:
+ headers using Etags will work incorrectly. (Issue 85).
+ * Added: Sabre_DAV_Auth_Backend_AbstractBasic class, which can be used to
+ easily implement basic authentication.
+ * Removed: Sabre_DAV_PermissionDenied class. Use Sabre_DAV_Forbidden
+ instead.
+ * Removed: Sabre_DAV_IDirectory interface, use Sabre_DAV_ICollection
+ instead.
+ * Added: Browser plugin now uses {DAV:}displayname if this property is
+ available.
+ * Added: Cache layer in the ObjectTree.
+ * Added: Tree classes now have a delete and getChildren method.
+ * Fixed: If-Modified-Since and If-Unmodified-Since would be incorrect if
+ the date is an exact match.
+ * Fixed: Support for multiple ETags in If-Match and If-None-Match headers.
+ * Fixed: Improved baseUrl handling.
+ * Fixed: Issue 67: Non-seekable stream support in ::put()/::get().
+ * Fixed: Issue 65: Invalid dates are now ignored.
+ * Updated: Refactoring in Sabre_CalDAV to make everything a bit more
+ ledgable.
+ * Fixed: Issue 88, Issue 89: Fixed compatibility for running SabreDAV on
+ Windows.
+ * Fixed: Issue 86: Fixed Content-Range top-boundary from 'file size' to
+ 'file size'-1.
+
+1.2.4 (2010-07-13)
+ * Fixed: Issue 62: Guessing baseUrl fails when url contains a
+ query-string.
+ * Added: Apache configuration sample for CGI/FastCGI setups.
+ * Fixed: Issue 64: Only returning calendar-data when it was actually
+ requested.
+
+1.2.3 (2010-06-26)
+ * Fixed: Issue 57: Supporting quotes around etags in If-Match and
+ If-None-Match
+
+1.2.2 (2010-06-21)
+ * Updated: SabreDAV now attempts to guess the BaseURI if it's not set.
+ * Updated: Better compatibility with BitKinex
+ * Fixed: Issue 56: Incorrect behaviour for If-None-Match headers and GET
+ requests.
+ * Fixed: Issue with certain encoded paths in Browser Plugin.
+
+1.2.1 (2010-06-07)
+ * Fixed: Issue 50, patch by Mattijs Hoitink.
+ * Fixed: Issue 51, Adding windows 7 lockfiles to TemporaryFileFilter.
+ * Fixed: Issue 38, Allowing custom filters to be added to
+ TemporaryFileFilter.
+ * Fixed: Issue 53, ETags in the If: header were always failing. This
+ behaviour is now corrected.
+ * Added: Apache Authentication backend, in case authentication through
+ .htaccess is desired.
+ * Updated: Small improvements to example files.
+
+1.2.0 (2010-05-24)
+ * Fixed: Browser plugin now displays international characters.
+ * Changed: More properties in CalDAV classes are now protected instead of
+ private.
+
+1.2.0beta3 (2010-05-14)
+ * Fixed: Custom properties were not properly sent back for allprops
+ requests.
+ * Fixed: Issue 49, incorrect parsing of PROPPATCH, affecting Office 2007.
+ * Changed: Removed CalDAV items from includes.php, and added a few missing
+ ones.
+
+1.2.0beta2 (2010-05-04)
+ * Fixed: Issue 46: Fatal error for some non-existent nodes.
+ * Updated: some example sql to include email address.
+ * Added: 208 and 508 statuscodes from RFC5842.
+ * Added: Apache2 configuration examples
+
+1.2.0beta1 (2010-04-28)
+ * Fixed: redundant namespace declaration in resourcetypes.
+ * Fixed: 2 locking bugs triggered by litmus when no Sabre_DAV_ILockable
+ interface is used.
+ * Changed: using http://sabredav.org/ns for all custom xml properties.
+ * Added: email address property to principals.
+ * Updated: CalendarObject validation.
+
+1.2.0alpha4 (2010-04-24)
+ * Added: Support for If-Range, If-Match, If-None-Match, If-Modified-Since,
+ If-Unmodified-Since.
+ * Changed: Brand new build system. Functionality is split up between
+ Sabre, Sabre_HTTP, Sabre_DAV and Sabre_CalDAV packages. In addition to
+ that a new non-pear package will be created with all this functionality
+ combined.
+ * Changed: Autoloader moved to Sabre/autoload.php.
+ * Changed: The Allow: header is now more accurate, with appropriate HTTP
+ methods per uri.
+ * Changed: Now throwing back Sabre_DAV_Exception_MethodNotAllowed on a few
+ places where Sabre_DAV_Exception_NotImplemented was used.
+
+1.2.0alpha3 (2010-04-20)
+ * Update: Complete rewrite of property updating. Now easier to use and
+ atomic.
+ * Fixed: Issue 16, automatically adding trailing / to baseUri.
+ * Added: text/plain is used for .txt files in GuessContentType plugin.
+ * Added: support for principal-property-search and
+ principal-search-property-set reports.
+ * Added: Issue 31: Hiding exception information by default. Can be turned
+ on with the Sabre_DAV_Server::$debugExceptions property.
+
+1.2.0alpha2 (2010-04-08)
+ * Added: Calendars are now private and can only be read by the owner.
+ * Fixed: double namespace declaration in multistatus responses.
+ * Added: MySQL database dumps. MySQL is now also supported next to SQLite.
+ * Added: expand-properties REPORT from RFC 3253.
+ * Added: Sabre_DAV_Property_IHref interface for properties exposing urls.
+ * Added: Issue 25: Throwing error on broken Finder behaviour.
+ * Changed: Authentication backend is now aware of current user.
+
+1.2.0alpha1 (2010-03-31)
+ * Fixed: Issue 26: Workaround for broken GVFS behaviour with encoded
+ special characters.
+ * Fixed: Issue 34: Incorrect Lock-Token response header for LOCK. Fixes
+ Office 2010 compatibility.
+ * Added: Issue 35: SabreDAV version to header to OPTIONS response to ease
+ debugging.
+ * Fixed: Issue 36: Incorrect variable name, throwing error in some
+ requests.
+ * Fixed: Issue 37: Incorrect smultron regex in temporary filefilter.
+ * Fixed: Issue 33: Converting ISO-8859-1 characters to UTF-8.
+ * Fixed: Issue 39 & Issue 40: Basename fails on non-utf-8 locales.
+ * Added: More unittests.
+ * Added: SabreDAV version to all error responses.
+ * Added: URLUtil class for decoding urls.
+ * Changed: Now using pear.sabredav.org pear channel.
+ * Changed: Sabre_DAV_Server::getCopyAndMoveInfo is now a public method.
+
+1.1.2-alpha (2010-03-18)
+ * Added: RFC5397 - current-user-principal support.
+ * Fixed: Issue 27: encoding entities in property responses.
+ * Added: naturalselection script now allows the user to specify a 'minimum
+ number of bytes' for deletion. This should reduce load due to less
+ crawling
+ * Added: Full support for the calendar-query report.
+ * Added: More unittests.
+ * Added: Support for complex property deserialization through the static
+ ::unserialize() method.
+ * Added: Support for modifying calendar-component-set
+ * Fixed: Issue 29: Added TIMEOUT_INFINITE constant
+
+1.1.1-alpha (2010-03-11)
+ * Added: RFC5689 - Extended MKCOL support.
+ * Fixed: Evolution support for CalDAV.
+ * Fixed: PDO-locks backend was pretty much completely broken. This is
+ 100% unittested now.
+ * Added: support for ctags.
+ * Fixed: Comma's between HTTP methods in 'Allow' method.
+ * Changed: default argument for Sabre_DAV_Locks_Backend_FS. This means a
+ datadirectory must always be specified from now on.
+ * Changed: Moved Sabre_DAV_Server::parseProps to
+ Sabre_DAV_XMLUtil::parseProperties.
+ * Changed: Sabre_DAV_IDirectory is now Sabre_DAV_ICollection.
+ * Changed: Sabre_DAV_Exception_PermissionDenied is now
+ Sabre_DAV_Exception_Forbidden.
+ * Changed: Sabre_CalDAV_ICalendarCollection is removed.
+ * Added: Sabre_DAV_IExtendedCollection.
+ * Added: Many more unittests.
+ * Added: support for calendar-timezone property.
+
+1.1.0-alpha (2010-03-01)
+ * Note: This version is forked from version 1.0.5, so release dates may be
+ out of order.
+ * Added: CalDAV - RFC 4791
+ * Removed: Sabre_PHP_Exception. PHP has a built-in ErrorException for
+ this.
+ * Added: PDO authentication backend.
+ * Added: Example sql for auth, caldav, locks for sqlite.
+ * Added: Sabre_DAV_Browser_GuessContentType plugin
+ * Changed: Authentication plugin refactored, making it possible to
+ implement non-digest authentication.
+ * Fixed: Better error display in browser plugin.
+ * Added: Support for {DAV:}supported-report-set
+ * Added: XML utility class with helper functions for the WebDAV protocol.
+ * Added: Tons of unittests
+ * Added: PrincipalCollection and Principal classes
+ * Added: Sabre_DAV_Server::getProperties for easy property retrieval
+ * Changed: {DAV:}resourceType defaults to 0
+ * Changed: Any non-null resourceType now gets a / appended to the href
+ value. Before this was just for {DAV:}collection's, but this is now also
+ the case for for example {DAV:}principal.
+ * Changed: The Href property class can now optionally create non-relative
+ uri's.
+ * Changed: Sabre_HTTP_Response now returns false if headers are already
+ sent and header-methods are called.
+ * Fixed: Issue 19: HEAD requests on Collections
+ * Fixed: Issue 21: Typo in Sabre_DAV_Property_Response
+ * Fixed: Issue 18: Doesn't work with Evolution Contacts
+
+1.0.15-stable (2010-05-28)
+ * Added: Issue 31: Hiding exception information by default. Can be turned
+ on with the Sabre_DAV_Server::$debugExceptions property.
+ * Added: Moved autoload from lib/ to lib/Sabre/autoload.php. This is also
+ the case in the upcoming 1.2.0, so it will improve future compatibility.
+
+1.0.14-stable (2010-04-15)
+ * Fixed: double namespace declaration in multistatus responses.
+
+1.0.13-stable (2010-03-30)
+ * Fixed: Issue 40: Last references to basename/dirname
+
+1.0.12-stable (2010-03-30)
+ * Fixed: Issue 37: Incorrect smultron regex in temporary filefilter.
+ * Fixed: Issue 26: Workaround for broken GVFS behaviour with encoded
+ special characters.
+ * Fixed: Issue 33: Converting ISO-8859-1 characters to UTF-8.
+ * Fixed: Issue 39: Basename fails on non-utf-8 locales.
+ * Added: More unittests.
+ * Added: SabreDAV version to all error responses.
+ * Added: URLUtil class for decoding urls.
+ * Updated: Now using pear.sabredav.org pear channel.
+
+1.0.11-stable (2010-03-23)
+ * Non-public release. This release is identical to 1.0.10, but it is used
+ to test releasing packages to pear.sabredav.org.
+
+1.0.10-stable (2010-03-22)
+ * Fixed: Issue 34: Invalid Lock-Token header response.
+ * Added: Issue 35: Addign SabreDAV version to HTTP OPTIONS responses.
+
+1.0.9-stable (2010-03-19)
+ * Fixed: Issue 27: Entities not being encoded in PROPFIND responses.
+ * Fixed: Issue 29: Added missing TIMEOUT_INFINITE constant.
+
+1.0.8-stable (2010-03-03)
+ * Fixed: Issue 21: typos causing errors
+ * Fixed: Issue 23: Comma's between methods in Allow header.
+ * Added: Sabre_DAV_ICollection interface, to aid in future compatibility.
+ * Added: Sabre_DAV_Exception_Forbidden exception. This will replace
+ Sabre_DAV_Exception_PermissionDenied in the future, and can already be
+ used to ensure future compatibility.
+
+1.0.7-stable (2010-02-24)
+ * Fixed: Issue 19 regression for MS Office
+
+1.0.6-stable (2010-02-23)
+ * Fixed: Issue 19: HEAD requests on Collections
+
+1.0.5-stable (2010-01-22)
+ * Fixed: Fatal error when a malformed url was used for unlocking, in
+ conjuction with Sabre.autoload.php due to a incorrect filename.
+ * Fixed: Improved unittests and build system
+
+1.0.4-stable (2010-01-11)
+ * Fixed: needed 2 different releases. One for googlecode and one for
+ pearfarm. This is to retain the old method to install SabreDAV until
+ pearfarm becomes the standard installation method.
+
+1.0.3-stable (2010-01-11)
+ * Added: RFC4709 support (davmount)
+ * Added: 6 unittests
+ * Added: naturalselection. A tool to keep cache directories below a
+ specified theshold.
+ * Changed: Now using pearfarm.org channel server.
+
+1.0.1-stable (2009-12-22)
+ * Fixed: Issue 15: typos in examples
+ * Fixed: Minor pear installation issues
+
+1.0.0-stable (2009-11-02)
+ * Added: SimpleDirectory class. This class allows creating static
+ directory structures with ease.
+ * Changed: Custom complex properties and exceptions now get an instance of
+ Sabre_DAV_Server as their first argument in serialize()
+ * Changed: Href complex property now prepends server's baseUri
+ * Changed: delete before an overwriting copy/move is now handles by server
+ class instead of tree classes
+ * Changed: events must now explicitly return false to stop execution.
+ Before, execution would be stopped by anything loosely evaluating to
+ false.
+ * Changed: the getPropertiesForPath method now takes a different set of
+ arguments, and returns a different response. This allows plugin
+ developers to return statuses for properties other than 200 and 404. The
+ hrefs are now also always calculated relative to the baseUri, and not
+ the uri of the request.
+ * Changed: generatePropFindResponse is renamed to generateMultiStatus, and
+ now takes a list of properties similar to the response of
+ getPropertiesForPath. This was also needed to improve flexibility for
+ plugin development.
+ * Changed: Auth plugins are no longer included. They were not yet stable
+ quality, so they will probably be reintroduced in a later version.
+ * Changed: PROPPATCH also used generateMultiStatus now.
+ * Removed: unknownProperties event. This is replaced by the
+ afterGetProperties event, which should provide more flexibility.
+ * Fixed: Only calling getSize() on IFile instances in httpHead()
+ * Added: beforeBind event. This is invoked upon file or directory creation
+ * Added: beforeWriteContent event, this is invoked by PUT and LOCK on an
+ existing resource.
+ * Added: beforeUnbind event. This is invoked right before deletion of any
+ resource.
+ * Added: afterGetProperties event. This event can be used to make
+ modifications to property responses.
+ * Added: beforeLock and beforeUnlock events.
+ * Added: afterBind event.
+ * Fixed: Copy and Move could fail in the root directory. This is now
+ fixed.
+ * Added: Plugins can now be retrieved by their classname. This is useful
+ for inter-plugin communication.
+ * Added: The Auth backend can now return usernames and user-id's.
+ * Added: The Auth backend got a getUsers method
+ * Added: Sabre_DAV_FSExt_Directory now returns quota info
+
+0.12.1-beta (2009-09-11)
+ * Fixed: UNLOCK bug. Unlock didn't work at all
+
+0.12-beta (2009-09-10)
+ * Updated: Browser plugin now shows multiple {DAV:}resourcetype values
+ if available.
+ * Added: Experimental PDO backend for Locks Manager
+ * Fixed: Sending Content-Length: 0 for every empty response. This
+ improves NGinx compatibility.
+ * Fixed: Last modification time is reported in UTC timezone. This improves
+ Finder compatibility.
+
+0.11-beta (2009-08-11)
+ * Updated: Now in Beta
+ * Updated: Pear package no longer includes docs/ directory. These just
+ contained rfc's, which are publically available. This reduces the
+ package from ~800k to ~60k
+ * Added: generatePropfindResponse now takes a baseUri argument
+ * Added: ResourceType property can now contain multiple resourcetypes.
+ * Fixed: Issue 13.
+
+0.10-alpha (2009-08-03)
+ * Added: Plugin to automatically map GET requests to non-files to
+ PROPFIND (Sabre_DAV_Browser_MapGetToPropFind). This should allow
+ easier debugging of complicated WebDAV setups.
+ * Added: Sabre_DAV_Property_Href class. For future use.
+ * Added: Ability to choose to use auth-int, auth or both for HTTP Digest
+ authentication. (Issue 11)
+ * Changed: Made more methods in Sabre_DAV_Server public.
+ * Fixed: TemporaryFileFilter plugin now intercepts HTTP LOCK requests
+ to non-existent files. (Issue 12)
+ * Added: Central list of defined xml namespace prefixes. This can reduce
+ Bandwidth and legibility for xml bodies with user-defined namespaces.
+ * Added: now a PEAR-compatible package again, thanks to Michael Gauthier
+ * Changed: moved default copy and move logic from ObjectTree to Tree class
+
+0.9-alpha (2009-07-21)
+ * Changed: Major refactoring, removed most of the logic from the Tree
+ objects. The Server class now directly works with the INode, IFile
+ and IDirectory objects. If you created your own Tree objects,
+ this will most likely break in this release.
+ * Changed: Moved all the Locking logic from the Tree and Server classes
+ into a separate plugin.
+ * Changed: TemporaryFileFilter is now a plugin.
+ * Added: Comes with an autoloader script. This can be used instead of
+ the includer script, and is preferred by some people.
+ * Added: AWS Authentication class.
+ * Added: simpleserversetup.py script. This will quickly get a fileserver
+ up and running.
+ * Added: When subscribing to events, it is now possible to supply a
+ priority. This is for example needed to ensure that the Authentication
+ Plugin is used before any other Plugin.
+ * Added: 22 new tests.
+ * Added: Users-manager plugin for .htdigest files. Experimental and
+ subject to change.
+ * Added: RFC 2324 HTTP 418 status code
+ * Fixed: Exclusive locks could in some cases be picked up as shared locks
+ * Fixed: Digest auth for non-apache servers had a bug (still not actually
+ tested this well).
+
+0.8-alpha (2009-05-30)
+ * Changed: Renamed all exceptions! This is a compatibility break. Every
+ Exception now follows Sabre_DAV_Exception_FileNotFound convention
+ instead of Sabre_DAV_FileNotFoundException.
+ * Added: Browser plugin now allows uploading and creating directories
+ straight from the browser.
+ * Added: 12 more unittests
+ * Fixed: Locking bug, which became prevalent on Windows Vista.
+ * Fixed: Netdrive support
+ * Fixed: TemporaryFileFilter filtered out too many files. Fixed some
+ of the regexes.
+ * Fixed: Added README and ChangeLog to package
+
+0.7-alpha (2009-03-29)
+ * Added: System to return complex properties from PROPFIND.
+ * Added: support for {DAV:}supportedlock.
+ * Added: support for {DAV:}lockdiscovery.
+ * Added: 6 new tests.
+ * Added: New plugin system.
+ * Added: Simple HTML directory plugin, for browser access.
+ * Added: Server class now sends back standard pre-condition error xml
+ bodies. This was new since RFC4918.
+ * Added: Sabre_DAV_Tree_Aggregrate, which can 'host' multiple Tree objects
+ into one.
+ * Added: simple basis for HTTP REPORT method. This method is not used yet,
+ but can be used by plugins to add reports.
+ * Changed: ->getSize is only called for files, no longer for collections.
+ r303
+ * Changed: Sabre_DAV_FilterTree is now Sabre_DAV_Tree_Filter
+ * Changed: Sabre_DAV_TemporaryFileFilter is now called
+ Sabre_DAV_Tree_TemporaryFileFilter.
+ * Changed: removed functions (get(/set)HTTPRequest(/Response)) from Server
+ class, and using a public property instead.
+ * Fixed: bug related to parsing proppatch and propfind requests. Didn't
+ show up in most clients, but it needed fixing regardless. (r255)
+ * Fixed: auth-int is now properly supported within HTTP Digest.
+ * Fixed: Using application/xml for a mimetype vs. text/xml as per RFC4918
+ sec 8.2.
+ * Fixed: TemporaryFileFilter now lets through GET's if they actually
+ exist on the backend. (r274)
+ * FIxed: Some methods didn't get passed through in the FilterTree (r283).
+ * Fixed: LockManager is now slightly more complex, Tree classes slightly
+ less. (r287)
+
+0.6-alpha (2009-02-16)
+ * Added: Now uses streams for files, instead of strings.
+ This means it won't require to hold entire files in memory, which can be
+ an issue if you're dealing with big files. Note that this breaks
+ compatibility for put() and createFile methods.
+ * Added: HTTP Digest Authentication helper class.
+ * Added: Support for HTTP Range header
+ * Added: Support for ETags within If: headers
+ * Added: The API can now return ETags and override the default Content-Type
+ * Added: starting with basic framework for unittesting, using PHPUnit.
+ * Added: 49 unittests.
+ * Added: Abstraction for the HTTP request.
+ * Updated: Using Clark Notation for tags in properties. This means tags
+ are serialized as {namespace}tagName instead of namespace#tagName
+ * Fixed: HTTP_BasicAuth class now works as expected.
+ * Fixed: DAV_Server uses / for a default baseUrl.
+ * Fixed: Last modification date is no longer ignored in PROPFIND.
+ * Fixed: PROPFIND now sends back information about the requestUri even
+ when "Depth: 1" is specified.
+
+0.5-alpha (2009-01-14)
+ * Added: Added a very simple example for implementing a mapping to PHP
+ file streams. This should allow easy implementation of for example a
+ WebDAV to FTP proxy.
+ * Added: HTTP Basic Authentication helper class.
+ * Added: Sabre_HTTP_Response class. This centralizes HTTP operations and
+ will be a start towards the creating of a testing framework.
+ * Updated: Backwards compatibility break: all require_once() statements
+ are removed
+ from all the files. It is now recommended to use autoloading of
+ classes, or just including lib/Sabre.includes.php. This fix was made
+ to allow easier integration into applications not using this standard
+ inclusion model.
+ * Updated: Better in-file documentation.
+ * Updated: Sabre_DAV_Tree can now work with Sabre_DAV_LockManager.
+ * Updated: Fixes a shared-lock bug.
+ * Updated: Removed ?> from the bottom of each php file.
+ * Updated: Split up some operations from Sabre_DAV_Server to
+ Sabre_HTTP_Response.
+ * Fixed: examples are now actually included in the pear package.
+
+0.4-alpha (2008-11-05)
+ * Passes all litmus tests!
+ * Added: more examples
+ * Added: Custom property support
+ * Added: Shared lock support
+ * Added: Depth support to locks
+ * Added: Locking on unmapped urls (non-existent nodes)
+ * Fixed: Advertising as WebDAV class 3 support
+
+0.3-alpha (2008-06-29)
+ * Fully working in MS Windows clients.
+ * Added: temporary file filter: support for smultron files.
+ * Added: Phing build scripts
+ * Added: PEAR package
+ * Fixed: MOVE bug identified using finder.
+ * Fixed: Using gzuncompress instead of gzdecode in the temporary file
+ filter. This seems more common.
+
+0.2-alpha (2008-05-27)
+ * Somewhat working in Windows clients
+ * Added: Working PROPPATCH method (doesn't support custom properties yet)
+ * Added: Temporary filename handling system
+ * Added: Sabre_DAV_IQuota to return quota information
+ * Added: PROPFIND now reads the request body and only supplies the
+ requested properties
+
+0.1-alpha (2008-04-04)
+ * First release!
+ * Passes litmus: basic, http and copymove test.
+ * Fully working in Finder and DavFSv2
+
+Project started: 2007-12-13
diff --git a/calendar/lib/SabreDAV/LICENSE b/calendar/lib/SabreDAV/LICENSE
new file mode 100644
index 0000000..68aceab
--- /dev/null
+++ b/calendar/lib/SabreDAV/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of SabreDAV nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/calendar/lib/SabreDAV/README.md b/calendar/lib/SabreDAV/README.md
new file mode 100644
index 0000000..8346c9a
--- /dev/null
+++ b/calendar/lib/SabreDAV/README.md
@@ -0,0 +1,30 @@
+# What is SabreDAV
+
+SabreDAV allows you to easily add WebDAV support to a PHP application. SabreDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API.
+
+### Feature list:
+
+* Fully WebDAV compliant
+* Supports Windows XP, Windows Vista, Mac OS/X, DavFSv2, Cadaver, Netdrive, Open Office, and probably more.
+* Passing all Litmus tests.
+* Supporting class 1, 2 and 3 Webdav servers.
+* Locking support.
+* Custom property support.
+* CalDAV (tested with [Evolution](http://code.google.com/p/sabredav/wiki/Evolution), [iCal](http://code.google.com/p/sabredav/wiki/ICal), [iPhone](http://code.google.com/p/sabredav/wiki/IPhone) and [Lightning](http://code.google.com/p/sabredav/wiki/Lightning)).
+* CardDAV (tested with [OS/X addressbook](http://code.google.com/p/sabredav/wiki/OSXAddressbook), the [iOS addressbook](http://code.google.com/p/sabredav/wiki/iOSCardDAV) and [Evolution](http://code.google.com/p/sabredav/wiki/Evolution)).
+* Over 97% unittest code coverage.
+
+### Supported RFC's:
+
+* [RFC2617](http://www.ietf.org/rfc/rfc2617.txt): Basic/Digest auth.
+* [RFC2518](http://www.ietf.org/rfc/rfc2518.txt): First WebDAV spec.
+* [RFC3744](http://www.ietf.org/rfc/rfc3744.txt): ACL (some features missing).
+* [RFC4709](http://www.ietf.org/rfc/rfc4709.txt): [DavMount](http://code.google.com/p/sabredav/wiki/DavMount).
+* [RFC4791](http://www.ietf.org/rfc/rfc4791.txt): CalDAV.
+* [RFC4918](http://www.ietf.org/rfc/rfc4918.txt): WebDAV revision.
+* [RFC5397](http://www.ietf.org/rfc/rfc5689.txt): current-user-principal.
+* [RFC5689](http://www.ietf.org/rfc/rfc5689.txt): Extended MKCOL.
+* [RFC5789](http://tools.ietf.org/html/rfc5789): PATCH method for HTTP.
+* [RFC6352](http://www.ietf.org/rfc/rfc6352.txt): CardDAV
+* [draft-daboo-carddav-directory-gateway](http://tools.ietf.org/html/draft-daboo-carddav-directory-gateway): CardDAV directory gateway
+* CalDAV ctag, CalDAV-proxy.
diff --git a/calendar/lib/SabreDAV/composer.json b/calendar/lib/SabreDAV/composer.json
new file mode 100644
index 0000000..f202785
--- /dev/null
+++ b/calendar/lib/SabreDAV/composer.json
@@ -0,0 +1,61 @@
+{
+ "name": "sabre/dav",
+ "type": "library",
+ "description": "WebDAV Framework for PHP",
+ "keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"],
+ "homepage": "http://code.google.com/p/sabredav/",
+ "license" : "BSD-3-Clause",
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "evert@rooftopsolutions.nl",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.1",
+ "sabre/vobject" : "~2.1.0",
+ "ext-dom": "*",
+ "ext-pcre": "*",
+ "ext-spl": "*",
+ "ext-simplexml": "*",
+ "ext-mbstring" : "*",
+ "ext-ctype" : "*",
+ "ext-date" : "*",
+ "ext-iconv" : "*",
+ "ext-libxml" : "*"
+ },
+ "require-dev" : {
+ "phpunit/phpunit" : "3.7.*",
+ "evert/phpdoc-md" : "~0.0.7",
+ "phing/phing" : "2.4.14"
+ },
+ "provide" : {
+ "evert/sabredav" : "1.7.*"
+ },
+ "suggest" : {
+ "ext-apc" : "*",
+ "ext-curl" : "*",
+ "ext-pdo" : "*"
+ },
+ "autoload": {
+ "psr-0" : {
+ "OldSabre\\DAV" : "lib/",
+ "OldSabre\\HTTP" : "lib/",
+ "OldSabre\\DAVACL" : "lib/",
+ "OldSabre\\CalDAV" : "lib/",
+ "OldSabre\\CardDAV" : "lib/"
+ }
+ },
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/evert/sabredav"
+ },
+ "bin" : [
+ "bin/sabredav"
+ ],
+ "config" : {
+ "bin-dir" : "./bin"
+ }
+}
diff --git a/calendar/lib/SabreDAV/composer.lock b/calendar/lib/SabreDAV/composer.lock
new file mode 100644
index 0000000..bea8d74
--- /dev/null
+++ b/calendar/lib/SabreDAV/composer.lock
@@ -0,0 +1,80 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
+ ],
+ "hash": "59cc5f06d39080610504a29c9398f95e",
+ "packages": [
+ {
+ "name": "sabre/vobject",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruux/sabre-vobject.git",
+ "reference": "2.1.0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/2.1.0",
+ "reference": "2.1.0",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.1"
+ },
+ "bin": [
+ "bin/vobjectvalidate.php"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "OldSabre\\VObject": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "evert@rooftopsolutions.nl",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "https://github.com/fruux/sabre-vobject",
+ "keywords": [
+ "VObject",
+ "iCalendar",
+ "vCard"
+ ],
+ "time": "2013-06-17 22:25:42"
+ }
+ ],
+ "packages-dev": null,
+ "aliases": [
+
+ ],
+ "minimum-stability": "stable",
+ "stability-flags": [
+
+ ],
+ "platform": {
+ "php": ">=5.3.1",
+ "ext-dom": "*",
+ "ext-pcre": "*",
+ "ext-spl": "*",
+ "ext-simplexml": "*",
+ "ext-mbstring": "*",
+ "ext-ctype": "*",
+ "ext-date": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*"
+ },
+ "platform-dev": [
+
+ ]
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php
new file mode 100644
index 0000000..2586441
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/AbstractBackend.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+use OldSabre\VObject;
+use OldSabre\CalDAV;
+
+/**
+ * Abstract Calendaring backend. Extend this class to create your own backends.
+ *
+ * Checkout the BackendInterface for all the methods that must be implemented.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => array(
+ * '{DAV:}displayname' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}owner' => null,
+ * )
+ * )
+ *
+ * In this example it was forbidden to update {DAV:}displayname.
+ * (403 Forbidden), which in turn also caused {DAV:}owner to fail
+ * (424 Failed Dependency) because the request needs to be atomic.
+ *
+ * @param mixed $calendarId
+ * @param array $mutations
+ * @return bool|array
+ */
+ public function updateCalendar($calendarId, array $mutations) {
+
+ return false;
+
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly adviced to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on either VEVENT or VTODO.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interprete all these filters can also simply
+ * be found in \OldSabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * @param mixed $calendarId
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery($calendarId, array $filters) {
+
+ $result = array();
+ $objects = $this->getCalendarObjects($calendarId);
+
+ $validator = new \OldSabre\CalDAV\CalendarQueryValidator();
+
+ foreach($objects as $object) {
+
+ if ($this->validateFilterForObject($object, $filters)) {
+ $result[] = $object['uri'];
+ }
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * This method validates if a filters (as passed to calendarQuery) matches
+ * the given object.
+ *
+ * @param array $object
+ * @param array $filters
+ * @return bool
+ */
+ protected function validateFilterForObject(array $object, array $filters) {
+
+ // Unfortunately, setting the 'calendardata' here is optional. If
+ // it was excluded, we actually need another call to get this as
+ // well.
+ if (!isset($object['calendardata'])) {
+ $object = $this->getCalendarObject($object['calendarid'], $object['uri']);
+ }
+
+ $data = is_resource($object['calendardata'])?stream_get_contents($object['calendardata']):$object['calendardata'];
+ $vObject = VObject\Reader::read($data);
+
+ $validator = new CalDAV\CalendarQueryValidator();
+ return $validator->validate($vObject, $filters);
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php
new file mode 100644
index 0000000..81f7778
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/BackendInterface.php
@@ -0,0 +1,233 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+/**
+ * Every CalDAV backend must at least implement this interface.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri, which the basename of the uri with which the calendar is
+ * accessed.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri);
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this calendar in other methods, such as updateCalendar.
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ * @param array $properties
+ * @return void
+ */
+ public function createCalendar($principalUri,$calendarUri,array $properties);
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => array(
+ * '{DAV:}displayname' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}owner' => null,
+ * )
+ * )
+ *
+ * In this example it was forbidden to update {DAV:}displayname.
+ * (403 Forbidden), which in turn also caused {DAV:}owner to fail
+ * (424 Failed Dependency) because the request needs to be atomic.
+ *
+ * @param mixed $calendarId
+ * @param array $mutations
+ * @return bool|array
+ */
+ public function updateCalendar($calendarId, array $mutations);
+
+ /**
+ * Delete a calendar and all it's objects
+ *
+ * @param mixed $calendarId
+ * @return void
+ */
+ public function deleteCalendar($calendarId);
+
+ /**
+ * Returns all calendar objects within a calendar.
+ *
+ * Every item contains an array with the following keys:
+ * * id - unique identifier which will be used for subsequent updates
+ * * calendardata - The iCalendar-compatible calendar data
+ * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
+ * * lastmodified - a timestamp of the last modification time
+ * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+ * ' "abcdef"')
+ * * calendarid - The calendarid as it was passed to this function.
+ * * size - The size of the calendar objects, in bytes.
+ *
+ * Note that the etag is optional, but it's highly encouraged to return for
+ * speed reasons.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * If neither etag or size are specified, the calendardata will be
+ * used/fetched to determine these numbers. If both are specified the
+ * amount of times this is needed is reduced by a great degree.
+ *
+ * @param mixed $calendarId
+ * @return array
+ */
+ public function getCalendarObjects($calendarId);
+
+ /**
+ * Returns information from a single calendar object, based on it's object
+ * uri.
+ *
+ * The returned array must have the same keys as getCalendarObjects. The
+ * 'calendardata' object is required here though, while it's not required
+ * for getCalendarObjects.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId,$objectUri);
+
+ /**
+ * Creates a new calendar object.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string|null
+ */
+ public function createCalendarObject($calendarId,$objectUri,$calendarData);
+
+ /**
+ * Updates an existing calendarobject, based on it's uri.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string|null
+ */
+ public function updateCalendarObject($calendarId,$objectUri,$calendarData);
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @return void
+ */
+ public function deleteCalendarObject($calendarId,$objectUri);
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly adviced to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on either VEVENT or VTODO.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interprete all these filters can also simply
+ * be found in OldSabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * @param mixed $calendarId
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery($calendarId, array $filters);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php
new file mode 100644
index 0000000..a99170e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/NotificationSupport.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+/**
+ * Adds caldav notification support to a backend.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * Notifications are defined at:
+ * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt
+ *
+ * These notifications are basically a list of server-generated notifications
+ * displayed to the user. Users can dismiss notifications by deleting them.
+ *
+ * The primary usecase is to allow for calendar-sharing.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface NotificationSupport extends BackendInterface {
+
+ /**
+ * Returns a list of notifications for a given principal url.
+ *
+ * The returned array should only consist of implementations of
+ * \OldSabre\CalDAV\Notifications\INotificationType.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getNotificationsForPrincipal($principalUri);
+
+ /**
+ * This deletes a specific notifcation.
+ *
+ * This may be called by a client once it deems a notification handled.
+ *
+ * @param string $principalUri
+ * @param \OldSabre\CalDAV\Notifications\INotificationType $notification
+ * @return void
+ */
+ public function deleteNotification($principalUri, \OldSabre\CalDAV\Notifications\INotificationType $notification);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php
new file mode 100644
index 0000000..104deea
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/PDO.php
@@ -0,0 +1,691 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+use OldSabre\VObject;
+use OldSabre\CalDAV;
+use OldSabre\DAV;
+
+/**
+ * PDO CalDAV backend
+ *
+ * This backend is used to store calendar-data in a PDO database, such as
+ * sqlite or MySQL
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * We need to specify a max date, because we need to stop *somewhere*
+ *
+ * On 32 bit system the maximum for a signed integer is 2147483647, so
+ * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
+ * in 2038-01-19 to avoid problems when the date is converted
+ * to a unix timestamp.
+ */
+ const MAX_DATE = '2038-01-01';
+
+ /**
+ * pdo
+ *
+ * @var \PDO
+ */
+ protected $pdo;
+
+ /**
+ * The table name that will be used for calendars
+ *
+ * @var string
+ */
+ protected $calendarTableName;
+
+ /**
+ * The table name that will be used for calendar objects
+ *
+ * @var string
+ */
+ protected $calendarObjectTableName;
+
+ /**
+ * List of CalDAV properties, and how they map to database fieldnames
+ * Add your own properties by simply adding on to this array.
+ *
+ * Note that only string-based properties are supported here.
+ *
+ * @var array
+ */
+ public $propertyMap = array(
+ '{DAV:}displayname' => 'displayname',
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
+ );
+
+ /**
+ * Creates the backend
+ *
+ * @param \PDO $pdo
+ * @param string $calendarTableName
+ * @param string $calendarObjectTableName
+ */
+ public function __construct(\PDO $pdo, $calendarTableName = 'calendars', $calendarObjectTableName = 'calendarobjects') {
+
+ $this->pdo = $pdo;
+ $this->calendarTableName = $calendarTableName;
+ $this->calendarObjectTableName = $calendarObjectTableName;
+
+ }
+
+ /**
+ * Returns a list of calendars for a principal.
+ *
+ * Every project is an array with the following keys:
+ * * id, a unique id that will be used by other functions to modify the
+ * calendar. This can be the same as the uri or a database key.
+ * * uri, which the basename of the uri with which the calendar is
+ * accessed.
+ * * principaluri. The owner of the calendar. Almost always the same as
+ * principalUri passed to this method.
+ *
+ * Furthermore it can contain webdav properties in clark notation. A very
+ * common one is '{DAV:}displayname'.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getCalendarsForUser($principalUri) {
+
+ $fields = array_values($this->propertyMap);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'ctag';
+ $fields[] = 'components';
+ $fields[] = 'principaluri';
+ $fields[] = 'transparent';
+
+ // Making fields a comma-delimited list
+ $fields = implode(', ', $fields);
+ $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM ".$this->calendarTableName." WHERE principaluri = ? ORDER BY calendarorder ASC");
+ $stmt->execute(array($principalUri));
+
+ $calendars = array();
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ $components = array();
+ if ($row['components']) {
+ $components = explode(',',$row['components']);
+ }
+
+ $calendar = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $row['ctag']?$row['ctag']:'0',
+ '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Property\SupportedCalendarComponentSet($components),
+ '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' => new CalDAV\Property\ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
+ );
+
+
+ foreach($this->propertyMap as $xmlName=>$dbName) {
+ $calendar[$xmlName] = $row[$dbName];
+ }
+
+ $calendars[] = $calendar;
+
+ }
+
+ return $calendars;
+
+ }
+
+ /**
+ * Creates a new calendar for a principal.
+ *
+ * If the creation was a success, an id must be returned that can be used to reference
+ * this calendar in other methods, such as updateCalendar
+ *
+ * @param string $principalUri
+ * @param string $calendarUri
+ * @param array $properties
+ * @return string
+ */
+ public function createCalendar($principalUri, $calendarUri, array $properties) {
+
+ $fieldNames = array(
+ 'principaluri',
+ 'uri',
+ 'ctag',
+ 'transparent',
+ );
+ $values = array(
+ ':principaluri' => $principalUri,
+ ':uri' => $calendarUri,
+ ':ctag' => 1,
+ ':transparent' => 0,
+ );
+
+ // Default value
+ $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+ $fieldNames[] = 'components';
+ if (!isset($properties[$sccs])) {
+ $values[':components'] = 'VEVENT,VTODO';
+ } else {
+ if (!($properties[$sccs] instanceof CalDAV\Property\SupportedCalendarComponentSet)) {
+ throw new DAV\Exception('The ' . $sccs . ' property must be of type: \OldSabre\CalDAV\Property\SupportedCalendarComponentSet');
+ }
+ $values[':components'] = implode(',',$properties[$sccs]->getValue());
+ }
+ $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
+ if (isset($properties[$transp])) {
+ $values[':transparent'] = $properties[$transp]->getValue()==='transparent';
+ }
+
+ foreach($this->propertyMap as $xmlName=>$dbName) {
+ if (isset($properties[$xmlName])) {
+
+ $values[':' . $dbName] = $properties[$xmlName];
+ $fieldNames[] = $dbName;
+ }
+ }
+
+ $stmt = $this->pdo->prepare("INSERT INTO ".$this->calendarTableName." (".implode(', ', $fieldNames).") VALUES (".implode(', ',array_keys($values)).")");
+ $stmt->execute($values);
+
+ return $this->pdo->lastInsertId();
+
+ }
+
+ /**
+ * Updates properties for a calendar.
+ *
+ * The mutations array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => array(
+ * '{DAV:}displayname' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}owner' => null,
+ * )
+ * )
+ *
+ * In this example it was forbidden to update {DAV:}displayname.
+ * (403 Forbidden), which in turn also caused {DAV:}owner to fail
+ * (424 Failed Dependency) because the request needs to be atomic.
+ *
+ * @param string $calendarId
+ * @param array $mutations
+ * @return bool|array
+ */
+ public function updateCalendar($calendarId, array $mutations) {
+
+ $newValues = array();
+ $result = array(
+ 200 => array(), // Ok
+ 403 => array(), // Forbidden
+ 424 => array(), // Failed Dependency
+ );
+
+ $hasError = false;
+
+ foreach($mutations as $propertyName=>$propertyValue) {
+
+ switch($propertyName) {
+ case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' :
+ $fieldName = 'transparent';
+ $newValues[$fieldName] = $propertyValue->getValue()==='transparent';
+ break;
+ default :
+ // Checking the property map
+ if (!isset($this->propertyMap[$propertyName])) {
+ // We don't know about this property.
+ $hasError = true;
+ $result[403][$propertyName] = null;
+ unset($mutations[$propertyName]);
+ continue;
+ }
+
+ $fieldName = $this->propertyMap[$propertyName];
+ $newValues[$fieldName] = $propertyValue;
+ }
+
+ }
+
+ // If there were any errors we need to fail the request
+ if ($hasError) {
+ // Properties has the remaining properties
+ foreach($mutations as $propertyName=>$propertyValue) {
+ $result[424][$propertyName] = null;
+ }
+
+ // Removing unused statuscodes for cleanliness
+ foreach($result as $status=>$properties) {
+ if (is_array($properties) && count($properties)===0) unset($result[$status]);
+ }
+
+ return $result;
+
+ }
+
+ // Success
+
+ // Now we're generating the sql query.
+ $valuesSql = array();
+ foreach($newValues as $fieldName=>$value) {
+ $valuesSql[] = $fieldName . ' = ?';
+ }
+ $valuesSql[] = 'ctag = ctag + 1';
+
+ $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ',$valuesSql) . " WHERE id = ?");
+ $newValues['id'] = $calendarId;
+ $stmt->execute(array_values($newValues));
+
+ return true;
+
+ }
+
+ /**
+ * Delete a calendar and all it's objects
+ *
+ * @param string $calendarId
+ * @return void
+ */
+ public function deleteCalendar($calendarId) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
+ $stmt->execute(array($calendarId));
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?');
+ $stmt->execute(array($calendarId));
+
+ }
+
+ /**
+ * Returns all calendar objects within a calendar.
+ *
+ * Every item contains an array with the following keys:
+ * * id - unique identifier which will be used for subsequent updates
+ * * calendardata - The iCalendar-compatible calendar data
+ * * uri - a unique key which will be used to construct the uri. This can be any arbitrary string.
+ * * lastmodified - a timestamp of the last modification time
+ * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+ * ' "abcdef"')
+ * * calendarid - The calendarid as it was passed to this function.
+ * * size - The size of the calendar objects, in bytes.
+ *
+ * Note that the etag is optional, but it's highly encouraged to return for
+ * speed reasons.
+ *
+ * The calendardata is also optional. If it's not returned
+ * 'getCalendarObject' will be called later, which *is* expected to return
+ * calendardata.
+ *
+ * If neither etag or size are specified, the calendardata will be
+ * used/fetched to determine these numbers. If both are specified the
+ * amount of times this is needed is reduced by a great degree.
+ *
+ * @param string $calendarId
+ * @return array
+ */
+ public function getCalendarObjects($calendarId) {
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?');
+ $stmt->execute(array($calendarId));
+
+ $result = array();
+ foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+ $result[] = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'calendarid' => $row['calendarid'],
+ 'size' => (int)$row['size'],
+ );
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Returns information from a single calendar object, based on it's object
+ * uri.
+ *
+ * The returned array must have the same keys as getCalendarObjects. The
+ * 'calendardata' object is required here though, while it's not required
+ * for getCalendarObjects.
+ *
+ * This method must return null if the object did not exist.
+ *
+ * @param string $calendarId
+ * @param string $objectUri
+ * @return array|null
+ */
+ public function getCalendarObject($calendarId,$objectUri) {
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
+ $stmt->execute(array($calendarId, $objectUri));
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+ if(!$row) return null;
+
+ return array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'lastmodified' => $row['lastmodified'],
+ 'etag' => '"' . $row['etag'] . '"',
+ 'calendarid' => $row['calendarid'],
+ 'size' => (int)$row['size'],
+ 'calendardata' => $row['calendardata'],
+ );
+
+ }
+
+
+ /**
+ * Creates a new calendar object.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string|null
+ */
+ public function createCalendarObject($calendarId,$objectUri,$calendarData) {
+
+ $extraData = $this->getDenormalizedData($calendarData);
+
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence) VALUES (?,?,?,?,?,?,?,?,?)');
+ $stmt->execute(array(
+ $calendarId,
+ $objectUri,
+ $calendarData,
+ time(),
+ $extraData['etag'],
+ $extraData['size'],
+ $extraData['componentType'],
+ $extraData['firstOccurence'],
+ $extraData['lastOccurence'],
+ ));
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt->execute(array($calendarId));
+
+ return '"' . $extraData['etag'] . '"';
+
+ }
+
+ /**
+ * Updates an existing calendarobject, based on it's uri.
+ *
+ * It is possible return an etag from this function, which will be used in
+ * the response to this PUT request. Note that the ETag must be surrounded
+ * by double-quotes.
+ *
+ * However, you should only really return this ETag if you don't mangle the
+ * calendar-data. If the result of a subsequent GET to this object is not
+ * the exact same as this request body, you should omit the ETag.
+ *
+ * @param mixed $calendarId
+ * @param string $objectUri
+ * @param string $calendarData
+ * @return string|null
+ */
+ public function updateCalendarObject($calendarId,$objectUri,$calendarData) {
+
+ $extraData = $this->getDenormalizedData($calendarData);
+
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ? WHERE calendarid = ? AND uri = ?');
+ $stmt->execute(array($calendarData,time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'] ,$calendarId,$objectUri));
+ $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt->execute(array($calendarId));
+
+ return '"' . $extraData['etag'] . '"';
+
+ }
+
+ /**
+ * Parses some information from calendar objects, used for optimized
+ * calendar-queries.
+ *
+ * Returns an array with the following keys:
+ * * etag
+ * * size
+ * * componentType
+ * * firstOccurence
+ * * lastOccurence
+ *
+ * @param string $calendarData
+ * @return array
+ */
+ protected function getDenormalizedData($calendarData) {
+
+ $vObject = VObject\Reader::read($calendarData);
+ $componentType = null;
+ $component = null;
+ $firstOccurence = null;
+ $lastOccurence = null;
+ foreach($vObject->getComponents() as $component) {
+ if ($component->name!=='VTIMEZONE') {
+ $componentType = $component->name;
+ break;
+ }
+ }
+ if (!$componentType) {
+ throw new \OldSabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+ }
+ if ($componentType === 'VEVENT') {
+ $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+ // Finding the last occurence is a bit harder
+ if (!isset($component->RRULE)) {
+ if (isset($component->DTEND)) {
+ $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+ } elseif (isset($component->DURATION)) {
+ $endDate = clone $component->DTSTART->getDateTime();
+ $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
+ $lastOccurence = $endDate->getTimeStamp();
+ } elseif (!$component->DTSTART->hasTime()) {
+ $endDate = clone $component->DTSTART->getDateTime();
+ $endDate->modify('+1 day');
+ $lastOccurence = $endDate->getTimeStamp();
+ } else {
+ $lastOccurence = $firstOccurence;
+ }
+ } else {
+ $it = new VObject\RecurrenceIterator($vObject, (string)$component->UID);
+ $maxDate = new \DateTime(self::MAX_DATE);
+ if ($it->isInfinite()) {
+ $lastOccurence = $maxDate->getTimeStamp();
+ } else {
+ $end = $it->getDtEnd();
+ while($it->valid() && $end < $maxDate) {
+ $end = $it->getDtEnd();
+ $it->next();
+
+ }
+ $lastOccurence = $end->getTimeStamp();
+ }
+
+ }
+ }
+
+ return array(
+ 'etag' => md5($calendarData),
+ 'size' => strlen($calendarData),
+ 'componentType' => $componentType,
+ 'firstOccurence' => $firstOccurence,
+ 'lastOccurence' => $lastOccurence,
+ );
+
+ }
+
+ /**
+ * Deletes an existing calendar object.
+ *
+ * @param string $calendarId
+ * @param string $objectUri
+ * @return void
+ */
+ public function deleteCalendarObject($calendarId,$objectUri) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?');
+ $stmt->execute(array($calendarId,$objectUri));
+ $stmt = $this->pdo->prepare('UPDATE '. $this->calendarTableName .' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt->execute(array($calendarId));
+
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * Note that it is extremely likely that getCalendarObject for every path
+ * returned from this method will be called almost immediately after. You
+ * may want to anticipate this to speed up these requests.
+ *
+ * This method provides a default implementation, which parses *all* the
+ * iCalendar objects in the specified calendar.
+ *
+ * This default may well be good enough for personal use, and calendars
+ * that aren't very large. But if you anticipate high usage, big calendars
+ * or high loads, you are strongly adviced to optimize certain paths.
+ *
+ * The best way to do so is override this method and to optimize
+ * specifically for 'common filters'.
+ *
+ * Requests that are extremely common are:
+ * * requests for just VEVENTS
+ * * requests for just VTODO
+ * * requests with a time-range-filter on a VEVENT.
+ *
+ * ..and combinations of these requests. It may not be worth it to try to
+ * handle every possible situation and just rely on the (relatively
+ * easy to use) CalendarQueryValidator to handle the rest.
+ *
+ * Note that especially time-range-filters may be difficult to parse. A
+ * time-range filter specified on a VEVENT must for instance also handle
+ * recurrence rules correctly.
+ * A good example of how to interprete all these filters can also simply
+ * be found in \OldSabre\CalDAV\CalendarQueryFilter. This class is as correct
+ * as possible, so it gives you a good idea on what type of stuff you need
+ * to think of.
+ *
+ * This specific implementation (for the PDO) backend optimizes filters on
+ * specific components, and VEVENT time-ranges.
+ *
+ * @param string $calendarId
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery($calendarId, array $filters) {
+
+ $result = array();
+ $validator = new \OldSabre\CalDAV\CalendarQueryValidator();
+
+ $componentType = null;
+ $requirePostFilter = true;
+ $timeRange = null;
+
+ // if no filters were specified, we don't need to filter after a query
+ if (!$filters['prop-filters'] && !$filters['comp-filters']) {
+ $requirePostFilter = false;
+ }
+
+ // Figuring out if there's a component filter
+ if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
+ $componentType = $filters['comp-filters'][0]['name'];
+
+ // Checking if we need post-filters
+ if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
+ $requirePostFilter = false;
+ }
+ // There was a time-range filter
+ if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
+ $timeRange = $filters['comp-filters'][0]['time-range'];
+
+ // If start time OR the end time is not specified, we can do a
+ // 100% accurate mysql query.
+ if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
+ $requirePostFilter = false;
+ }
+ }
+
+ }
+
+ if ($requirePostFilter) {
+ $query = "SELECT uri, calendardata FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid";
+ } else {
+ $query = "SELECT uri FROM ".$this->calendarObjectTableName." WHERE calendarid = :calendarid";
+ }
+
+ $values = array(
+ 'calendarid' => $calendarId,
+ );
+
+ if ($componentType) {
+ $query.=" AND componenttype = :componenttype";
+ $values['componenttype'] = $componentType;
+ }
+
+ if ($timeRange && $timeRange['start']) {
+ $query.=" AND lastoccurence > :startdate";
+ $values['startdate'] = $timeRange['start']->getTimeStamp();
+ }
+ if ($timeRange && $timeRange['end']) {
+ $query.=" AND firstoccurence < :enddate";
+ $values['enddate'] = $timeRange['end']->getTimeStamp();
+ }
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ $result = array();
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($requirePostFilter) {
+ if (!$this->validateFilterForObject($row, $filters)) {
+ continue;
+ }
+ }
+ $result[] = $row['uri'];
+
+ }
+
+ return $result;
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php
new file mode 100644
index 0000000..626eade
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Backend/SharingSupport.php
@@ -0,0 +1,243 @@
+<?php
+
+namespace OldSabre\CalDAV\Backend;
+
+/**
+ * Adds support for sharing features to a CalDAV server.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * Early warning: Currently SabreDAV provides no implementation for this. This
+ * is, because in it's current state there is no elegant way to do this.
+ * The problem lies in the fact that a real CalDAV server with sharing support
+ * would first need email support (with invite notifications), and really also
+ * a browser-frontend that allows people to accept or reject these shares.
+ *
+ * In addition, the CalDAV backends are currently kept as independent as
+ * possible, and should not be aware of principals, email addresses or
+ * accounts.
+ *
+ * Adding an implementation for Sharing to standard-sabredav would contradict
+ * these goals, so for this reason this is currently not implemented, although
+ * it may very well in the future; but probably not before SabreDAV 2.0.
+ *
+ * The interface works however, so if you implement all this, and do it
+ * correctly sharing _will_ work. It's not particularly easy, and I _urge you_
+ * to make yourself acquainted with the following document first:
+ *
+ * https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
+ *
+ * An overview
+ * ===========
+ *
+ * Implementing this interface will allow a user to share his or her calendars
+ * to other users. Effectively, when a calendar is shared the calendar will
+ * show up in both the Sharer's and Sharee's calendar-home root.
+ * This interface adds a few methods that ensure that this happens, and there
+ * are also a number of new requirements in the base-class you must now follow.
+ *
+ *
+ * How it works
+ * ============
+ *
+ * When a user shares a calendar, the updateShares() method will be called with
+ * a list of sharees that are now added, and a list of sharees that have been
+ * removed.
+ * Removal is instant, but when a sharee is added the sharee first gets a
+ * chance to accept or reject the invitation for a share.
+ *
+ * After a share is accepted, the calendar will be returned from
+ * getUserCalendars for both the sharer, and the sharee.
+ *
+ * If the sharee deletes the calendar, only their share gets deleted. When the
+ * owner deletes a calendar, it will be removed for everybody.
+ *
+ *
+ * Notifications
+ * =============
+ *
+ * During all these sharing operations, a lot of notifications are sent back
+ * and forward.
+ *
+ * Whenever the list of sharees for a calendar has been changed (they have been
+ * added, removed or modified) all sharees should get a notification for this
+ * change.
+ * This notification is always represented by:
+ *
+ * OldSabre\CalDAV\Notifications\Notification\Invite
+ *
+ * In the case of an invite, the sharee may reply with an 'accept' or
+ * 'decline'. These are always represented by:
+ *
+ * OldSabre\CalDAV\Notifications\Notification\Invite
+ *
+ *
+ * Calendar access by sharees
+ * ==========================
+ *
+ * As mentioned earlier, shared calendars must now also be returned for
+ * getCalendarsForUser for sharees. A few things change though.
+ *
+ * The following properties must be specified:
+ *
+ * 1. {http://calendarserver.org/ns/}shared-url
+ *
+ * This property MUST contain the url to the original calendar, that is.. the
+ * path to the calendar from the owner.
+ *
+ * 2. {http://sabredav.org/ns}owner-principal
+ *
+ * This is a url to to the principal who is sharing the calendar.
+ *
+ * 3. {http://sabredav.org/ns}read-only
+ *
+ * This should be either 0 or 1, depending on if the user has read-only or
+ * read-write access to the calendar.
+ *
+ * Only when this is done, the calendar will correctly be marked as a calendar
+ * that's shared to him, thus allowing clients to display the correct interface
+ * and ACL enforcement.
+ *
+ * If a sharee deletes their calendar, only their instance of the calendar
+ * should be deleted, the original should still exists.
+ * Pretty much any 'dead' WebDAV properties on these shared calendars should be
+ * specific to a user. This means that if the displayname is changed by a
+ * sharee, the original is not affected. This is also true for:
+ * * The description
+ * * The color
+ * * The order
+ * * And any other dead properties.
+ *
+ * Properties like a ctag should not be different for multiple instances of the
+ * calendar.
+ *
+ * Lastly, objects *within* calendars should also have user-specific data. The
+ * two things that are user-specific are:
+ * * VALARM objects
+ * * The TRANSP property
+ *
+ * This _also_ implies that if a VALARM is deleted by a sharee for some event,
+ * this has no effect on the original VALARM.
+ *
+ * Understandably, the this last requirement is one of the hardest.
+ * Realisticly, I can see people ignoring this part of the spec, but that could
+ * cause a different set of issues.
+ *
+ *
+ * Publishing
+ * ==========
+ *
+ * When a user publishes a url, the server should generate a 'publish url'.
+ * This is a read-only url, anybody can use to consume the calendar feed.
+ *
+ * Calendars are in one of two states:
+ * * published
+ * * unpublished
+ *
+ * If a calendar is published, the following property should be returned
+ * for each calendar in getCalendarsForPrincipal.
+ *
+ * {http://calendarserver.org/ns/}publish-url
+ *
+ * This element should contain a {DAV:}href element, which points to the
+ * public url that does not require authentication. Unlike every other href,
+ * this url must be absolute.
+ *
+ * Ideally, the following property is always returned
+ *
+ * {http://calendarserver.org/ns/}pre-publish-url
+ *
+ * This property should contain the url that the calendar _would_ have, if it
+ * were to be published. iCal uses this to display the url, before the user
+ * will actually publish it.
+ *
+ *
+ * Selectively disabling publish or share feature
+ * ==============================================
+ *
+ * If OldSabre\CalDAV\Property\AllowedSharingModes is returned from
+ * getCalendarsByUser, this allows the server to specify whether either sharing,
+ * or publishing is supported.
+ *
+ * This allows a client to determine in advance which features are available,
+ * and update the interface appropriately. If this property is not returned by
+ * the backend, the SharingPlugin automatically injects it and assumes both
+ * features are available.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SharingSupport extends NotificationSupport {
+
+ /**
+ * Updates the list of shares.
+ *
+ * The first array is a list of people that are to be added to the
+ * calendar.
+ *
+ * Every element in the add array has the following properties:
+ * * href - A url. Usually a mailto: address
+ * * commonName - Usually a first and last name, or false
+ * * summary - A description of the share, can also be false
+ * * readOnly - A boolean value
+ *
+ * Every element in the remove array is just the address string.
+ *
+ * Note that if the calendar is currently marked as 'not shared' by and
+ * this method is called, the calendar should be 'upgraded' to a shared
+ * calendar.
+ *
+ * @param mixed $calendarId
+ * @param array $add
+ * @param array $remove
+ * @return void
+ */
+ function updateShares($calendarId, array $add, array $remove);
+
+ /**
+ * Returns the list of people whom this calendar is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * This method may be called by either the original instance of the
+ * calendar, as well as the shared instances. In the case of the shared
+ * instances, it is perfectly acceptable to return an empty array in case
+ * there are privacy concerns.
+ *
+ * @param mixed $calendarId
+ * @return array
+ */
+ function getShares($calendarId);
+
+ /**
+ * This method is called when a user replied to a request to share.
+ *
+ * If the user chose to accept the share, this method should return the
+ * newly created calendar url.
+ *
+ * @param string href The sharee who is replying (often a mailto: address)
+ * @param int status One of the SharingPlugin::STATUS_* constants
+ * @param string $calendarUri The url to the calendar thats being shared
+ * @param string $inReplyTo The unique id this message is a response to
+ * @param string $summary A description of the reply
+ * @return null|string
+ */
+ function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null);
+
+ /**
+ * Publishes a calendar
+ *
+ * @param mixed $calendarId
+ * @param bool $value
+ * @return void
+ */
+ function setPublishStatus($calendarId, $value);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php
new file mode 100644
index 0000000..bfd4990
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Calendar.php
@@ -0,0 +1,376 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * This object represents a CalDAV calendar.
+ *
+ * A calendar can contain multiple TODO and or Events. These are represented
+ * as \OldSabre\CalDAV\CalendarObject objects.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Calendar implements ICalendar, DAV\IProperties, DAVACL\IACL {
+
+ /**
+ * This is an array with calendar information
+ *
+ * @var array
+ */
+ protected $calendarInfo;
+
+ /**
+ * CalDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $calendarInfo
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->calendarInfo = $calendarInfo;
+
+ }
+
+ /**
+ * Returns the name of the calendar
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->calendarInfo['uri'];
+
+ }
+
+ /**
+ * Updates properties such as the display name and description
+ *
+ * @param array $mutations
+ * @return array
+ */
+ public function updateProperties($mutations) {
+
+ return $this->caldavBackend->updateCalendar($this->calendarInfo['id'],$mutations);
+
+ }
+
+ /**
+ * Returns the list of properties
+ *
+ * @param array $requestedProperties
+ * @return array
+ */
+ public function getProperties($requestedProperties) {
+
+ $response = array();
+
+ foreach($requestedProperties as $prop) switch($prop) {
+
+ case '{urn:ietf:params:xml:ns:caldav}supported-calendar-data' :
+ $response[$prop] = new Property\SupportedCalendarData();
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}supported-collation-set' :
+ $response[$prop] = new Property\SupportedCollationSet();
+ break;
+ case '{DAV:}owner' :
+ $response[$prop] = new DAVACL\Property\Principal(DAVACL\Property\Principal::HREF,$this->calendarInfo['principaluri']);
+ break;
+ default :
+ if (isset($this->calendarInfo[$prop])) $response[$prop] = $this->calendarInfo[$prop];
+ break;
+
+ }
+ return $response;
+
+ }
+
+ /**
+ * Returns a calendar object
+ *
+ * The contained calendar objects are for example Events or Todo's.
+ *
+ * @param string $name
+ * @return \OldSabre\CalDAV\ICalendarObject
+ */
+ public function getChild($name) {
+
+ $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name);
+
+ if (!$obj) throw new DAV\Exception\NotFound('Calendar object not found');
+
+ $obj['acl'] = $this->getACL();
+ // Removing the irrelivant
+ foreach($obj['acl'] as $key=>$acl) {
+ if ($acl['privilege'] === '{' . Plugin::NS_CALDAV . '}read-free-busy') {
+ unset($obj['acl'][$key]);
+ }
+ }
+
+ return new CalendarObject($this->caldavBackend,$this->calendarInfo,$obj);
+
+ }
+
+ /**
+ * Returns the full list of calendar objects
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
+ $children = array();
+ foreach($objs as $obj) {
+ $obj['acl'] = $this->getACL();
+ // Removing the irrelivant
+ foreach($obj['acl'] as $key=>$acl) {
+ if ($acl['privilege'] === '{' . Plugin::NS_CALDAV . '}read-free-busy') {
+ unset($obj['acl'][$key]);
+ }
+ }
+ $children[] = new CalendarObject($this->caldavBackend,$this->calendarInfo,$obj);
+ }
+ return $children;
+
+ }
+
+ /**
+ * Checks if a child-node exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'],$name);
+ if (!$obj)
+ return false;
+ else
+ return true;
+
+ }
+
+ /**
+ * Creates a new directory
+ *
+ * We actually block this, as subdirectories are not allowed in calendars.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
+
+ }
+
+ /**
+ * Creates a new file
+ *
+ * The contents of the new file must be a valid ICalendar string.
+ *
+ * @param string $name
+ * @param resource $calendarData
+ * @return string|null
+ */
+ public function createFile($name,$calendarData = null) {
+
+ if (is_resource($calendarData)) {
+ $calendarData = stream_get_contents($calendarData);
+ }
+ return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'],$name,$calendarData);
+
+ }
+
+ /**
+ * Deletes the calendar.
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->caldavBackend->deleteCalendar($this->calendarInfo['id']);
+
+ }
+
+ /**
+ * Renames the calendar. Note that most calendars use the
+ * {DAV:}displayname to display a name to display a name.
+ *
+ * @param string $newName
+ * @return void
+ */
+ public function setName($newName) {
+
+ throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ *
+ * @return void
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->calendarInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner() . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner() . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
+ 'principal' => '{DAV:}authenticated',
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See \OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+
+ // We need to inject 'read-free-busy' in the tree, aggregated under
+ // {DAV:}read.
+ foreach($default['aggregates'] as &$agg) {
+
+ if ($agg['privilege'] !== '{DAV:}read') continue;
+
+ $agg['aggregates'][] = array(
+ 'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
+ );
+
+ }
+ return $default;
+
+ }
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery(array $filters) {
+
+ return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php
new file mode 100644
index 0000000..a60da8f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarObject.php
@@ -0,0 +1,279 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * The CalendarObject represents a single VEVENT or VTODO within a Calendar.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarObject extends \OldSabre\DAV\File implements ICalendarObject, \OldSabre\DAVACL\IACL {
+
+ /**
+ * OldSabre\CalDAV\Backend\BackendInterface
+ *
+ * @var OldSabre\CalDAV\Backend\AbstractBackend
+ */
+ protected $caldavBackend;
+
+ /**
+ * Array with information about this CalendarObject
+ *
+ * @var array
+ */
+ protected $objectData;
+
+ /**
+ * Array with information about the containing calendar
+ *
+ * @var array
+ */
+ protected $calendarInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $calendarInfo
+ * @param array $objectData
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend,array $calendarInfo,array $objectData) {
+
+ $this->caldavBackend = $caldavBackend;
+
+ if (!isset($objectData['calendarid'])) {
+ throw new \InvalidArgumentException('The objectData argument must contain a \'calendarid\' property');
+ }
+ if (!isset($objectData['uri'])) {
+ throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
+ }
+
+ $this->calendarInfo = $calendarInfo;
+ $this->objectData = $objectData;
+
+ }
+
+ /**
+ * Returns the uri for this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->objectData['uri'];
+
+ }
+
+ /**
+ * Returns the ICalendar-formatted object
+ *
+ * @return string
+ */
+ public function get() {
+
+ // Pre-populating the 'calendardata' is optional, if we don't have it
+ // already we fetch it from the backend.
+ if (!isset($this->objectData['calendardata'])) {
+ $this->objectData = $this->caldavBackend->getCalendarObject($this->objectData['calendarid'], $this->objectData['uri']);
+ }
+ return $this->objectData['calendardata'];
+
+ }
+
+ /**
+ * Updates the ICalendar-formatted object
+ *
+ * @param string|resource $calendarData
+ * @return string
+ */
+ public function put($calendarData) {
+
+ if (is_resource($calendarData)) {
+ $calendarData = stream_get_contents($calendarData);
+ }
+ $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'],$this->objectData['uri'],$calendarData);
+ $this->objectData['calendardata'] = $calendarData;
+ $this->objectData['etag'] = $etag;
+
+ return $etag;
+
+ }
+
+ /**
+ * Deletes the calendar object
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'],$this->objectData['uri']);
+
+ }
+
+ /**
+ * Returns the mime content-type
+ *
+ * @return string
+ */
+ public function getContentType() {
+
+ return 'text/calendar; charset=utf-8';
+
+ }
+
+ /**
+ * Returns an ETag for this object.
+ *
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ if (isset($this->objectData['etag'])) {
+ return $this->objectData['etag'];
+ } else {
+ return '"' . md5($this->get()). '"';
+ }
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return $this->objectData['lastmodified'];
+
+ }
+
+ /**
+ * Returns the size of this object in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ if (array_key_exists('size',$this->objectData)) {
+ return $this->objectData['size'];
+ } else {
+ return strlen($this->get());
+ }
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->calendarInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ // An alternative acl may be specified in the object data.
+ if (isset($this->objectData['acl'])) {
+ return $this->objectData['acl'];
+ }
+
+ // The default ACL
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new \OldSabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See \OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php
new file mode 100644
index 0000000..a2a2b75
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryParser.php
@@ -0,0 +1,298 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\VObject;
+
+/**
+ * Parses the calendar-query report request body.
+ *
+ * Whoever designed this format, and the CalDAV equivalent even more so,
+ * has no feel for design.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarQueryParser {
+
+ /**
+ * List of requested properties the client wanted
+ *
+ * @var array
+ */
+ public $requestedProperties;
+
+ /**
+ * List of property/component filters.
+ *
+ * @var array
+ */
+ public $filters;
+
+ /**
+ * This property will contain null if CALDAV:expand was not specified,
+ * otherwise it will contain an array with 2 elements (start, end). Each
+ * contain a DateTime object.
+ *
+ * If expand is specified, recurring calendar objects are to be expanded
+ * into their individual components, and only the components that fall
+ * within the specified time-range are to be returned.
+ *
+ * For more details, see rfc4791, section 9.6.5.
+ *
+ * @var null|array
+ */
+ public $expand;
+
+ /**
+ * DOM Document
+ *
+ * @var DOMDocument
+ */
+ protected $dom;
+
+ /**
+ * DOM XPath object
+ *
+ * @var DOMXPath
+ */
+ protected $xpath;
+
+ /**
+ * Creates the parser
+ *
+ * @param \DOMDocument $dom
+ */
+ public function __construct(\DOMDocument $dom) {
+
+ $this->dom = $dom;
+ $this->xpath = new \DOMXPath($dom);
+ $this->xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
+ $this->xpath->registerNameSpace('dav','urn:DAV');
+
+ }
+
+ /**
+ * Parses the request.
+ *
+ * @return void
+ */
+ public function parse() {
+
+ $filterNode = null;
+
+ $filter = $this->xpath->query('/cal:calendar-query/cal:filter');
+ if ($filter->length !== 1) {
+ throw new \OldSabre\DAV\Exception\BadRequest('Only one filter element is allowed');
+ }
+
+ $compFilters = $this->parseCompFilters($filter->item(0));
+ if (count($compFilters)!==1) {
+ throw new \OldSabre\DAV\Exception\BadRequest('There must be exactly 1 top-level comp-filter.');
+ }
+
+ $this->filters = $compFilters[0];
+ $this->requestedProperties = array_keys(\OldSabre\DAV\XMLUtil::parseProperties($this->dom->firstChild));
+
+ $expand = $this->xpath->query('/cal:calendar-query/dav:prop/cal:calendar-data/cal:expand');
+ if ($expand->length>0) {
+ $this->expand = $this->parseExpand($expand->item(0));
+ }
+
+
+ }
+
+ /**
+ * Parses all the 'comp-filter' elements from a node
+ *
+ * @param \DOMElement $parentNode
+ * @return array
+ */
+ protected function parseCompFilters(\DOMElement $parentNode) {
+
+ $compFilterNodes = $this->xpath->query('cal:comp-filter', $parentNode);
+ $result = array();
+
+ for($ii=0; $ii < $compFilterNodes->length; $ii++) {
+
+ $compFilterNode = $compFilterNodes->item($ii);
+
+ $compFilter = array();
+ $compFilter['name'] = $compFilterNode->getAttribute('name');
+ $compFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $compFilterNode)->length>0;
+ $compFilter['comp-filters'] = $this->parseCompFilters($compFilterNode);
+ $compFilter['prop-filters'] = $this->parsePropFilters($compFilterNode);
+ $compFilter['time-range'] = $this->parseTimeRange($compFilterNode);
+
+ if ($compFilter['time-range'] && !in_array($compFilter['name'],array(
+ 'VEVENT',
+ 'VTODO',
+ 'VJOURNAL',
+ 'VFREEBUSY',
+ 'VALARM',
+ ))) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The time-range filter is not defined for the ' . $compFilter['name'] . ' component');
+ };
+
+ $result[] = $compFilter;
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Parses all the prop-filter elements from a node
+ *
+ * @param \DOMElement $parentNode
+ * @return array
+ */
+ protected function parsePropFilters(\DOMElement $parentNode) {
+
+ $propFilterNodes = $this->xpath->query('cal:prop-filter', $parentNode);
+ $result = array();
+
+ for ($ii=0; $ii < $propFilterNodes->length; $ii++) {
+
+ $propFilterNode = $propFilterNodes->item($ii);
+ $propFilter = array();
+ $propFilter['name'] = $propFilterNode->getAttribute('name');
+ $propFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $propFilterNode)->length>0;
+ $propFilter['param-filters'] = $this->parseParamFilters($propFilterNode);
+ $propFilter['text-match'] = $this->parseTextMatch($propFilterNode);
+ $propFilter['time-range'] = $this->parseTimeRange($propFilterNode);
+
+ $result[] = $propFilter;
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Parses the param-filter element
+ *
+ * @param \DOMElement $parentNode
+ * @return array
+ */
+ protected function parseParamFilters(\DOMElement $parentNode) {
+
+ $paramFilterNodes = $this->xpath->query('cal:param-filter', $parentNode);
+ $result = array();
+
+ for($ii=0;$ii<$paramFilterNodes->length;$ii++) {
+
+ $paramFilterNode = $paramFilterNodes->item($ii);
+ $paramFilter = array();
+ $paramFilter['name'] = $paramFilterNode->getAttribute('name');
+ $paramFilter['is-not-defined'] = $this->xpath->query('cal:is-not-defined', $paramFilterNode)->length>0;
+ $paramFilter['text-match'] = $this->parseTextMatch($paramFilterNode);
+
+ $result[] = $paramFilter;
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Parses the text-match element
+ *
+ * @param \DOMElement $parentNode
+ * @return array|null
+ */
+ protected function parseTextMatch(\DOMElement $parentNode) {
+
+ $textMatchNodes = $this->xpath->query('cal:text-match', $parentNode);
+
+ if ($textMatchNodes->length === 0)
+ return null;
+
+ $textMatchNode = $textMatchNodes->item(0);
+ $negateCondition = $textMatchNode->getAttribute('negate-condition');
+ $negateCondition = $negateCondition==='yes';
+ $collation = $textMatchNode->getAttribute('collation');
+ if (!$collation) $collation = 'i;ascii-casemap';
+
+ return array(
+ 'negate-condition' => $negateCondition,
+ 'collation' => $collation,
+ 'value' => $textMatchNode->nodeValue
+ );
+
+ }
+
+ /**
+ * Parses the time-range element
+ *
+ * @param \DOMElement $parentNode
+ * @return array|null
+ */
+ protected function parseTimeRange(\DOMElement $parentNode) {
+
+ $timeRangeNodes = $this->xpath->query('cal:time-range', $parentNode);
+ if ($timeRangeNodes->length === 0) {
+ return null;
+ }
+
+ $timeRangeNode = $timeRangeNodes->item(0);
+
+ if ($start = $timeRangeNode->getAttribute('start')) {
+ $start = VObject\DateTimeParser::parseDateTime($start);
+ } else {
+ $start = null;
+ }
+ if ($end = $timeRangeNode->getAttribute('end')) {
+ $end = VObject\DateTimeParser::parseDateTime($end);
+ } else {
+ $end = null;
+ }
+
+ if (!is_null($start) && !is_null($end) && $end <= $start) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the time-range filter');
+ }
+
+ return array(
+ 'start' => $start,
+ 'end' => $end,
+ );
+
+ }
+
+ /**
+ * Parses the CALDAV:expand element
+ *
+ * @param \DOMElement $parentNode
+ * @return void
+ */
+ protected function parseExpand(\DOMElement $parentNode) {
+
+ $start = $parentNode->getAttribute('start');
+ if(!$start) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The "start" attribute is required for the CALDAV:expand element');
+ }
+ $start = VObject\DateTimeParser::parseDateTime($start);
+
+ $end = $parentNode->getAttribute('end');
+ if(!$end) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The "end" attribute is required for the CALDAV:expand element');
+ }
+
+ $end = VObject\DateTimeParser::parseDateTime($end);
+
+ if ($end <= $start) {
+ throw new \OldSabre\DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
+ }
+
+ return array(
+ 'start' => $start,
+ 'end' => $end,
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php
new file mode 100644
index 0000000..581a126
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarQueryValidator.php
@@ -0,0 +1,392 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\VObject;
+use DateTime;
+
+/**
+ * CalendarQuery Validator
+ *
+ * This class is responsible for checking if an iCalendar object matches a set
+ * of filters. The main function to do this is 'validate'.
+ *
+ * This is used to determine which icalendar objects should be returned for a
+ * calendar-query REPORT request.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarQueryValidator {
+
+ /**
+ * Verify if a list of filters applies to the calendar data object
+ *
+ * The list of filters must be formatted as parsed by \OldSabre\CalDAV\CalendarQueryParser
+ *
+ * @param VObject\Component $vObject
+ * @param array $filters
+ * @return bool
+ */
+ public function validate(VObject\Component $vObject,array $filters) {
+
+ // The top level object is always a component filter.
+ // We'll parse it manually, as it's pretty simple.
+ if ($vObject->name !== $filters['name']) {
+ return false;
+ }
+
+ return
+ $this->validateCompFilters($vObject, $filters['comp-filters']) &&
+ $this->validatePropFilters($vObject, $filters['prop-filters']);
+
+
+ }
+
+ /**
+ * This method checks the validity of comp-filters.
+ *
+ * A list of comp-filters needs to be specified. Also the parent of the
+ * component we're checking should be specified, not the component to check
+ * itself.
+ *
+ * @param VObject\Component $parent
+ * @param array $filters
+ * @return bool
+ */
+ protected function validateCompFilters(VObject\Component $parent, array $filters) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($parent->$filter['name']);
+
+ if ($filter['is-not-defined']) {
+
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if ($filter['time-range']) {
+ foreach($parent->$filter['name'] as $subComponent) {
+ if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
+ continue 2;
+ }
+ }
+ return false;
+ }
+
+ if (!$filter['comp-filters'] && !$filter['prop-filters']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one component
+ // for which the subfilters hold true.
+ foreach($parent->$filter['name'] as $subComponent) {
+
+ if (
+ $this->validateCompFilters($subComponent, $filter['comp-filters']) &&
+ $this->validatePropFilters($subComponent, $filter['prop-filters'])) {
+ // We had a match, so this comp-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ // If we got here it means there were sub-comp-filters or
+ // sub-prop-filters and there was no match. This means this filter
+ // needs to return false.
+ return false;
+
+ }
+
+ // If we got here it means we got through all comp-filters alive so the
+ // filters were all true.
+ return true;
+
+ }
+
+ /**
+ * This method checks the validity of prop-filters.
+ *
+ * A list of prop-filters needs to be specified. Also the parent of the
+ * property we're checking should be specified, not the property to check
+ * itself.
+ *
+ * @param VObject\Component $parent
+ * @param array $filters
+ * @return bool
+ */
+ protected function validatePropFilters(VObject\Component $parent, array $filters) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($parent->$filter['name']);
+
+ if ($filter['is-not-defined']) {
+
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if ($filter['time-range']) {
+ foreach($parent->$filter['name'] as $subComponent) {
+ if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
+ continue 2;
+ }
+ }
+ return false;
+ }
+
+ if (!$filter['param-filters'] && !$filter['text-match']) {
+ continue;
+ }
+
+ // If there are sub-filters, we need to find at least one property
+ // for which the subfilters hold true.
+ foreach($parent->$filter['name'] as $subComponent) {
+
+ if(
+ $this->validateParamFilters($subComponent, $filter['param-filters']) &&
+ (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
+ ) {
+ // We had a match, so this prop-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ // If we got here it means there were sub-param-filters or
+ // text-match filters and there was no match. This means the
+ // filter needs to return false.
+ return false;
+
+ }
+
+ // If we got here it means we got through all prop-filters alive so the
+ // filters were all true.
+ return true;
+
+ }
+
+ /**
+ * This method checks the validity of param-filters.
+ *
+ * A list of param-filters needs to be specified. Also the parent of the
+ * parameter we're checking should be specified, not the parameter to check
+ * itself.
+ *
+ * @param VObject\Property $parent
+ * @param array $filters
+ * @return bool
+ */
+ protected function validateParamFilters(VObject\Property $parent, array $filters) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($parent[$filter['name']]);
+
+ if ($filter['is-not-defined']) {
+
+ if ($isDefined) {
+ return false;
+ } else {
+ continue;
+ }
+
+ }
+ if (!$isDefined) {
+ return false;
+ }
+
+ if (!$filter['text-match']) {
+ continue;
+ }
+
+ if (version_compare(VObject\Version::VERSION, '3.0.0beta1', '>=')) {
+
+ // If there are sub-filters, we need to find at least one parameter
+ // for which the subfilters hold true.
+ foreach($parent[$filter['name']]->getParts() as $subParam) {
+
+ if($this->validateTextMatch($subParam,$filter['text-match'])) {
+ // We had a match, so this param-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ } else {
+
+ // If there are sub-filters, we need to find at least one parameter
+ // for which the subfilters hold true.
+ foreach($parent[$filter['name']] as $subParam) {
+
+ if($this->validateTextMatch($subParam,$filter['text-match'])) {
+ // We had a match, so this param-filter succeeds
+ continue 2;
+ }
+
+ }
+
+ }
+
+ // If we got here it means there was a text-match filter and there
+ // were no matches. This means the filter needs to return false.
+ return false;
+
+ }
+
+ // If we got here it means we got through all param-filters alive so the
+ // filters were all true.
+ return true;
+
+ }
+
+ /**
+ * This method checks the validity of a text-match.
+ *
+ * A single text-match should be specified as well as the specific property
+ * or parameter we need to validate.
+ *
+ * @param VObject\Node|string $check Value to check against.
+ * @param array $textMatch
+ * @return bool
+ */
+ protected function validateTextMatch($check, array $textMatch) {
+
+ if ($check instanceof VObject\Node) {
+ $check = (string)$check;
+ }
+
+ $isMatching = \OldSabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
+
+ return ($textMatch['negate-condition'] xor $isMatching);
+
+ }
+
+ /**
+ * Validates if a component matches the given time range.
+ *
+ * This is all based on the rules specified in rfc4791, which are quite
+ * complex.
+ *
+ * @param VObject\Node $component
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ protected function validateTimeRange(VObject\Node $component, $start, $end) {
+
+ if (is_null($start)) {
+ $start = new DateTime('1900-01-01');
+ }
+ if (is_null($end)) {
+ $end = new DateTime('3000-01-01');
+ }
+
+ switch($component->name) {
+
+ case 'VEVENT' :
+ case 'VTODO' :
+ case 'VJOURNAL' :
+
+ return $component->isInTimeRange($start, $end);
+
+ case 'VALARM' :
+
+ // If the valarm is wrapped in a recurring event, we need to
+ // expand the recursions, and validate each.
+ //
+ // Our datamodel doesn't easily allow us to do this straight
+ // in the VALARM component code, so this is a hack, and an
+ // expensive one too.
+ if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
+
+ // Fire up the iterator!
+ $it = new VObject\RecurrenceIterator($component->parent->parent, (string)$component->parent->UID);
+ while($it->valid()) {
+ $expandedEvent = $it->getEventObject();
+
+ // We need to check from these expanded alarms, which
+ // one is the first to trigger. Based on this, we can
+ // determine if we can 'give up' expanding events.
+ $firstAlarm = null;
+ if ($expandedEvent->VALARM !== null) {
+ foreach($expandedEvent->VALARM as $expandedAlarm) {
+
+ $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
+ if ($expandedAlarm->isInTimeRange($start, $end)) {
+ return true;
+ }
+
+ if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
+ // This is an alarm with a non-relative trigger
+ // time, likely created by a buggy client. The
+ // implication is that every alarm in this
+ // recurring event trigger at the exact same
+ // time. It doesn't make sense to traverse
+ // further.
+ } else {
+ // We store the first alarm as a means to
+ // figure out when we can stop traversing.
+ if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
+ $firstAlarm = $effectiveTrigger;
+ }
+ }
+ }
+ }
+ if (is_null($firstAlarm)) {
+ // No alarm was found.
+ //
+ // Or technically: No alarm that will change for
+ // every instance of the recurrence was found,
+ // which means we can assume there was no match.
+ return false;
+ }
+ if ($firstAlarm > $end) {
+ return false;
+ }
+ $it->next();
+ }
+ return false;
+ } else {
+ return $component->isInTimeRange($start, $end);
+ }
+
+ case 'VFREEBUSY' :
+ throw new \OldSabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
+
+ case 'COMPLETED' :
+ case 'CREATED' :
+ case 'DTEND' :
+ case 'DTSTAMP' :
+ case 'DTSTART' :
+ case 'DUE' :
+ case 'LAST-MODIFIED' :
+ return ($start <= $component->getDateTime() && $end >= $component->getDateTime());
+
+
+
+ default :
+ throw new \OldSabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php
new file mode 100644
index 0000000..810a249
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/CalendarRootNode.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAVACL\PrincipalBackend;
+
+/**
+ * Calendars collection
+ *
+ * This object is responsible for generating a list of calendar-homes for each
+ * user.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarRootNode extends \OldSabre\DAVACL\AbstractPrincipalCollection {
+
+ /**
+ * CalDAV backend
+ *
+ * @var OldSabre\CalDAV\Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Constructor
+ *
+ * This constructor needs both an authentication and a caldav backend.
+ *
+ * By default this class will show a list of calendar collections for
+ * principals in the 'principals' collection. If your main principals are
+ * actually located in a different path, use the $principalPrefix argument
+ * to override this.
+ *
+ * @param PrincipalBackend\BackendInterface $principalBackend
+ * @param Backend\BackendInterface $caldavBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend,Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals') {
+
+ parent::__construct($principalBackend, $principalPrefix);
+ $this->caldavBackend = $caldavBackend;
+
+ }
+
+ /**
+ * Returns the nodename
+ *
+ * We're overriding this, because the default will be the 'principalPrefix',
+ * and we want it to be OldSabre\CalDAV\Plugin::CALENDAR_ROOT
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return Plugin::CALENDAR_ROOT;
+
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \OldSabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal) {
+
+ return new UserCalendars($this->caldavBackend, $principal);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php
new file mode 100644
index 0000000..c335fcd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Exception/InvalidComponentType.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\CalDAV\Exception;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * InvalidComponentType
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InvalidComponentType extends DAV\Exception\Forbidden {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {CALDAV:}supported-calendar-component as defined in rfc4791
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV,'cal:supported-calendar-component');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php
new file mode 100644
index 0000000..4a5a75b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICSExportPlugin.php
@@ -0,0 +1,142 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\VObject;
+
+/**
+ * ICS Exporter
+ *
+ * This plugin adds the ability to export entire calendars as .ics files.
+ * This is useful for clients that don't support CalDAV yet. They often do
+ * support ics files.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ICSExportPlugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to Server class
+ *
+ * @var \OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and registers event handlers
+ *
+ * @param \OldSabre\DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90);
+
+ }
+
+ /**
+ * 'beforeMethod' event handles. This event handles intercepts GET requests ending
+ * with ?export
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ if ($method!='GET') return;
+ if ($this->server->httpRequest->getQueryString()!='export') return;
+
+ // splitting uri
+ list($uri) = explode('?',$uri,2);
+
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ if (!($node instanceof Calendar)) return;
+
+ // Checking ACL, if available.
+ if ($aclPlugin = $this->server->getPlugin('acl')) {
+ $aclPlugin->checkPrivileges($uri, '{DAV:}read');
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type','text/calendar');
+ $this->server->httpResponse->sendStatus(200);
+
+ $nodes = $this->server->getPropertiesForPath($uri, array(
+ '{' . Plugin::NS_CALDAV . '}calendar-data',
+ ),1);
+
+ $this->server->httpResponse->sendBody($this->generateICS($nodes));
+
+ // Returning false to break the event chain
+ return false;
+
+ }
+
+ /**
+ * Merges all calendar objects, and builds one big ics export
+ *
+ * @param array $nodes
+ * @return string
+ */
+ public function generateICS(array $nodes) {
+
+ $calendar = new VObject\Component\VCalendar();
+ $calendar->version = '2.0';
+ if (DAV\Server::$exposeVersion) {
+ $calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
+ } else {
+ $calendar->prodid = '-//SabreDAV//SabreDAV//EN';
+ }
+ $calendar->calscale = 'GREGORIAN';
+
+ $collectedTimezones = array();
+
+ $timezones = array();
+ $objects = array();
+
+ foreach($nodes as $node) {
+
+ if (!isset($node[200]['{' . Plugin::NS_CALDAV . '}calendar-data'])) {
+ continue;
+ }
+ $nodeData = $node[200]['{' . Plugin::NS_CALDAV . '}calendar-data'];
+
+ $nodeComp = VObject\Reader::read($nodeData);
+
+ foreach($nodeComp->children() as $child) {
+
+ switch($child->name) {
+ case 'VEVENT' :
+ case 'VTODO' :
+ case 'VJOURNAL' :
+ $objects[] = $child;
+ break;
+
+ // VTIMEZONE is special, because we need to filter out the duplicates
+ case 'VTIMEZONE' :
+ // Naively just checking tzid.
+ if (in_array((string)$child->TZID, $collectedTimezones)) continue;
+
+ $timezones[] = $child;
+ $collectedTimezones[] = $child->TZID;
+ break;
+
+ }
+
+ }
+
+ }
+
+ foreach($timezones as $tz) $calendar->add($tz);
+ foreach($objects as $obj) $calendar->add($obj);
+
+ return $calendar->serialize();
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php
new file mode 100644
index 0000000..70916bc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendar.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OldSabre\CalDAV;
+use OldSabre\DAV;
+
+/**
+ * Calendar interface
+ *
+ * Implement this interface to allow a node to be recognized as an calendar.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendar extends DAV\ICollection {
+
+ /**
+ * Performs a calendar-query on the contents of this calendar.
+ *
+ * The calendar-query is defined in RFC4791 : CalDAV. Using the
+ * calendar-query it is possible for a client to request a specific set of
+ * object, based on contents of iCalendar properties, date-ranges and
+ * iCalendar component types (VTODO, VEVENT).
+ *
+ * This method should just return a list of (relative) urls that match this
+ * query.
+ *
+ * The list of filters are specified as an array. The exact array is
+ * documented by \OldSabre\CalDAV\CalendarQueryParser.
+ *
+ * @param array $filters
+ * @return array
+ */
+ public function calendarQuery(array $filters);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php
new file mode 100644
index 0000000..4e5dcd7
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ICalendarObject.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\CalDAV;
+use OldSabre\DAV;
+
+/**
+ * CalendarObject interface
+ *
+ * Extend the ICalendarObject interface to allow your custom nodes to be picked up as
+ * CalendarObjects.
+ *
+ * Calendar objects are resources such as Events, Todo's or Journals.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendarObject extends DAV\IFile {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php
new file mode 100644
index 0000000..c8fed62
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/IShareableCalendar.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This interface represents a Calendar that can be shared with other users.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IShareableCalendar extends ICalendar {
+
+ /**
+ * Updates the list of shares.
+ *
+ * The first array is a list of people that are to be added to the
+ * calendar.
+ *
+ * Every element in the add array has the following properties:
+ * * href - A url. Usually a mailto: address
+ * * commonName - Usually a first and last name, or false
+ * * summary - A description of the share, can also be false
+ * * readOnly - A boolean value
+ *
+ * Every element in the remove array is just the address string.
+ *
+ * @param array $add
+ * @param array $remove
+ * @return void
+ */
+ function updateShares(array $add, array $remove);
+
+ /**
+ * Returns the list of people whom this calendar is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ function getShares();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php
new file mode 100644
index 0000000..05ebe79
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ISharedCalendar.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This interface represents a Calendar that is shared by a different user.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ISharedCalendar extends ICalendar {
+
+ /**
+ * This method should return the url of the owners' copy of the shared
+ * calendar.
+ *
+ * @return string
+ */
+ function getSharedUrl();
+
+ /**
+ * Returns the list of people whom this calendar is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ function getShares();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php
new file mode 100644
index 0000000..d18a409
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Collection.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+use OldSabre\DAVACL;
+
+/**
+ * This node represents a list of notifications.
+ *
+ * It provides no additional functionality, but you must implement this
+ * interface to allow the Notifications plugin to mark the collection
+ * as a notifications collection.
+ *
+ * This collection should only return OldSabre\CalDAV\Notifications\INode nodes as
+ * its children.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Collection extends DAV\Collection implements ICollection, DAVACL\IACL {
+
+ /**
+ * The notification backend
+ *
+ * @var OldSabre\CalDAV\Backend\NotificationSupport
+ */
+ protected $caldavBackend;
+
+ /**
+ * Principal uri
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param CalDAV\Backend\NotificationSupport $caldavBackend
+ * @param string $principalUri
+ */
+ public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns all notifications for a principal
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $children = array();
+ $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri);
+
+ foreach($notifications as $notification) {
+
+ $children[] = new Node(
+ $this->caldavBackend,
+ $this->principalUri,
+ $notification
+ );
+ }
+
+ return $children;
+
+ }
+
+ /**
+ * Returns the name of this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'notifications';
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'principal' => $this->getOwner(),
+ 'privilege' => '{DAV:}read',
+ 'protected' => true,
+ ),
+ array(
+ 'principal' => $this->getOwner(),
+ 'privilege' => '{DAV:}write',
+ 'protected' => true,
+ )
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's as an array argument.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php
new file mode 100644
index 0000000..cabdac4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/ICollection.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+use OldSabre\DAV;
+
+/**
+ * This node represents a list of notifications.
+ *
+ * It provides no additional functionality, but you must implement this
+ * interface to allow the Notifications plugin to mark the collection
+ * as a notifications collection.
+ *
+ * This collection should only return OldSabre\CalDAV\Notifications\INode nodes as
+ * its children.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICollection extends DAV\ICollection {
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php
new file mode 100644
index 0000000..f63f8cc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INode.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+/**
+ * This node represents a single notification.
+ *
+ * The signature is mostly identical to that of OldSabre\DAV\IFile, but the get() method
+ * MUST return an xml document that matches the requirements of the
+ * 'caldav-notifications.txt' spec.
+ *
+ * For a complete example, check out the Notification class, which contains
+ * some helper functions.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INode {
+
+ /**
+ * This method must return an xml element, using the
+ * OldSabre\CalDAV\Notifications\INotificationType classes.
+ *
+ * @return INotificationType
+ */
+ function getNotificationType();
+
+ /**
+ * Returns the etag for the notification.
+ *
+ * The etag must be surrounded by litteral double-quotes.
+ *
+ * @return string
+ */
+ function getETag();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php
new file mode 100644
index 0000000..ef3b3e9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/INotificationType.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+use OldSabre\DAV;
+
+/**
+ * This interface reflects a single notification type.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INotificationType extends DAV\PropertyInterface {
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ function serializeBody(DAV\Server $server, \DOMElement $node);
+
+ /**
+ * Returns a unique id for this notification
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ function getId();
+
+ /**
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ function getETag();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php
new file mode 100644
index 0000000..c9b88a4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Node.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+use OldSabre\DAVACL;
+
+/**
+ * This node represents a single notification.
+ *
+ * The signature is mostly identical to that of OldSabre\DAV\IFile, but the get() method
+ * MUST return an xml document that matches the requirements of the
+ * 'caldav-notifications.txt' spec.
+
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Node extends DAV\File implements INode, DAVACL\IACL {
+
+ /**
+ * The notification backend
+ *
+ * @var OldSabre\CalDAV\Backend\NotificationSupport
+ */
+ protected $caldavBackend;
+
+ /**
+ * The actual notification
+ *
+ * @var OldSabre\CalDAV\Notifications\INotificationType
+ */
+ protected $notification;
+
+ /**
+ * Owner principal of the notification
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param CalDAV\Backend\NotificationSupport $caldavBackend
+ * @param string $principalUri
+ * @param CalDAV\Notifications\INotificationType $notification
+ */
+ public function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri, INotificationType $notification) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->principalUri = $principalUri;
+ $this->notification = $notification;
+
+ }
+
+ /**
+ * Returns the path name for this notification
+ *
+ * @return id
+ */
+ public function getName() {
+
+ return $this->notification->getId() . '.xml';
+
+ }
+
+ /**
+ * Returns the etag for the notification.
+ *
+ * The etag must be surrounded by litteral double-quotes.
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ return $this->notification->getETag();
+
+ }
+
+ /**
+ * This method must return an xml element, using the
+ * OldSabre\CalDAV\Notifications\INotificationType classes.
+ *
+ * @return INotificationType
+ */
+ public function getNotificationType() {
+
+ return $this->notification;
+
+ }
+
+ /**
+ * Deletes this notification
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'principal' => $this->getOwner(),
+ 'privilege' => '{DAV:}read',
+ 'protected' => true,
+ ),
+ array(
+ 'principal' => $this->getOwner(),
+ 'privilege' => '{DAV:}write',
+ 'protected' => true,
+ )
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's as an array argument.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php
new file mode 100644
index 0000000..3c8fbf5
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/Invite.php
@@ -0,0 +1,324 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications\Notification;
+
+use OldSabre\CalDAV\SharingPlugin as SharingPlugin;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * This class represents the cs:invite-notification notification element.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Invite extends DAV\Property implements CalDAV\Notifications\INotificationType {
+
+ /**
+ * A unique id for the message
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * Timestamp of the notification
+ *
+ * @var DateTime
+ */
+ protected $dtStamp;
+
+ /**
+ * A url to the recipient of the notification. This can be an email
+ * address (mailto:), or a principal url.
+ *
+ * @var string
+ */
+ protected $href;
+
+ /**
+ * The type of message, see the SharingPlugin::STATUS_* constants.
+ *
+ * @var int
+ */
+ protected $type;
+
+ /**
+ * True if access to a calendar is read-only.
+ *
+ * @var bool
+ */
+ protected $readOnly;
+
+ /**
+ * A url to the shared calendar.
+ *
+ * @var string
+ */
+ protected $hostUrl;
+
+ /**
+ * Url to the sharer of the calendar
+ *
+ * @var string
+ */
+ protected $organizer;
+
+ /**
+ * The name of the sharer.
+ *
+ * @var string
+ */
+ protected $commonName;
+
+ /**
+ * The name of the sharer.
+ *
+ * @var string
+ */
+ protected $firstName;
+
+ /**
+ * The name of the sharer.
+ *
+ * @var string
+ */
+ protected $lastName;
+
+ /**
+ * A description of the share request
+ *
+ * @var string
+ */
+ protected $summary;
+
+ /**
+ * The Etag for the notification
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * The list of supported components
+ *
+ * @var OldSabre\CalDAV\Property\SupportedCalendarComponentSet
+ */
+ protected $supportedComponents;
+
+ /**
+ * Creates the Invite notification.
+ *
+ * This constructor receives an array with the following elements:
+ *
+ * * id - A unique id
+ * * etag - The etag
+ * * dtStamp - A DateTime object with a timestamp for the notification.
+ * * type - The type of notification, see SharingPlugin::STATUS_*
+ * constants for details.
+ * * readOnly - This must be set to true, if this is an invite for
+ * read-only access to a calendar.
+ * * hostUrl - A url to the shared calendar.
+ * * organizer - Url to the sharer principal.
+ * * commonName - The real name of the sharer (optional).
+ * * firstName - The first name of the sharer (optional).
+ * * lastName - The last name of the sharer (optional).
+ * * summary - Description of the share, can be the same as the
+ * calendar, but may also be modified (optional).
+ * * supportedComponents - An instance of
+ * OldSabre\CalDAV\Property\SupportedCalendarComponentSet.
+ * This allows the client to determine which components
+ * will be supported in the shared calendar. This is
+ * also optional.
+ *
+ * @param array $values All the options
+ */
+ public function __construct(array $values) {
+
+ $required = array(
+ 'id',
+ 'etag',
+ 'href',
+ 'dtStamp',
+ 'type',
+ 'readOnly',
+ 'hostUrl',
+ 'organizer',
+ );
+ foreach($required as $item) {
+ if (!isset($values[$item])) {
+ throw new \InvalidArgumentException($item . ' is a required constructor option');
+ }
+ }
+
+ foreach($values as $key=>$value) {
+ if (!property_exists($this, $key)) {
+ throw new \InvalidArgumentException('Unknown option: ' . $key);
+ }
+ $this->$key = $value;
+ }
+
+ }
+
+ /**
+ * Serializes the notification as a single property.
+ *
+ * You should usually just encode the single top-level element of the
+ * notification.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $node) {
+
+ $prop = $node->ownerDocument->createElement('cs:invite-notification');
+ $node->appendChild($prop);
+
+ }
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serializeBody(DAV\Server $server, \DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $dt = $doc->createElement('cs:dtstamp');
+ $this->dtStamp->setTimezone(new \DateTimezone('GMT'));
+ $dt->appendChild($doc->createTextNode($this->dtStamp->format('Ymd\\THis\\Z')));
+ $node->appendChild($dt);
+
+ $prop = $doc->createElement('cs:invite-notification');
+ $node->appendChild($prop);
+
+ $uid = $doc->createElement('cs:uid');
+ $uid->appendChild( $doc->createTextNode($this->id) );
+ $prop->appendChild($uid);
+
+ $href = $doc->createElement('d:href');
+ $href->appendChild( $doc->createTextNode( $this->href ) );
+ $prop->appendChild($href);
+
+ $nodeName = null;
+ switch($this->type) {
+
+ case SharingPlugin::STATUS_ACCEPTED :
+ $nodeName = 'cs:invite-accepted';
+ break;
+ case SharingPlugin::STATUS_DECLINED :
+ $nodeName = 'cs:invite-declined';
+ break;
+ case SharingPlugin::STATUS_DELETED :
+ $nodeName = 'cs:invite-deleted';
+ break;
+ case SharingPlugin::STATUS_NORESPONSE :
+ $nodeName = 'cs:invite-noresponse';
+ break;
+
+ }
+ $prop->appendChild(
+ $doc->createElement($nodeName)
+ );
+ $hostHref = $doc->createElement('d:href', $server->getBaseUri() . $this->hostUrl);
+ $hostUrl = $doc->createElement('cs:hosturl');
+ $hostUrl->appendChild($hostHref);
+ $prop->appendChild($hostUrl);
+
+ $access = $doc->createElement('cs:access');
+ if ($this->readOnly) {
+ $access->appendChild($doc->createElement('cs:read'));
+ } else {
+ $access->appendChild($doc->createElement('cs:read-write'));
+ }
+ $prop->appendChild($access);
+
+ $organizerUrl = $doc->createElement('cs:organizer');
+ // If the organizer contains a 'mailto:' part, it means it should be
+ // treated as absolute.
+ if (strtolower(substr($this->organizer,0,7))==='mailto:') {
+ $organizerHref = new DAV\Property\Href($this->organizer, false);
+ } else {
+ $organizerHref = new DAV\Property\Href($this->organizer, true);
+ }
+ $organizerHref->serialize($server, $organizerUrl);
+
+ if ($this->commonName) {
+ $commonName = $doc->createElement('cs:common-name');
+ $commonName->appendChild($doc->createTextNode($this->commonName));
+ $organizerUrl->appendChild($commonName);
+
+ $commonNameOld = $doc->createElement('cs:organizer-cn');
+ $commonNameOld->appendChild($doc->createTextNode($this->commonName));
+ $prop->appendChild($commonNameOld);
+
+ }
+ if ($this->firstName) {
+ $firstName = $doc->createElement('cs:first-name');
+ $firstName->appendChild($doc->createTextNode($this->firstName));
+ $organizerUrl->appendChild($firstName);
+
+ $firstNameOld = $doc->createElement('cs:organizer-first');
+ $firstNameOld->appendChild($doc->createTextNode($this->firstName));
+ $prop->appendChild($firstNameOld);
+ }
+ if ($this->lastName) {
+ $lastName = $doc->createElement('cs:last-name');
+ $lastName->appendChild($doc->createTextNode($this->lastName));
+ $organizerUrl->appendChild($lastName);
+
+ $lastNameOld = $doc->createElement('cs:organizer-last');
+ $lastNameOld->appendChild($doc->createTextNode($this->lastName));
+ $prop->appendChild($lastNameOld);
+ }
+ $prop->appendChild($organizerUrl);
+
+ if ($this->summary) {
+ $summary = $doc->createElement('cs:summary');
+ $summary->appendChild($doc->createTextNode($this->summary));
+ $prop->appendChild($summary);
+ }
+ if ($this->supportedComponents) {
+
+ $xcomp = $doc->createElement('cal:supported-calendar-component-set');
+ $this->supportedComponents->serialize($server, $xcomp);
+ $prop->appendChild($xcomp);
+
+ }
+
+ }
+
+ /**
+ * Returns a unique id for this notification
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ public function getId() {
+
+ return $this->id;
+
+ }
+
+ /**
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ return $this->etag;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php
new file mode 100644
index 0000000..540da86
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/InviteReply.php
@@ -0,0 +1,218 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications\Notification;
+
+use OldSabre\CalDAV\SharingPlugin as SharingPlugin;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * This class represents the cs:invite-reply notification element.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InviteReply extends DAV\Property implements CalDAV\Notifications\INotificationType {
+
+ /**
+ * A unique id for the message
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * Timestamp of the notification
+ *
+ * @var DateTime
+ */
+ protected $dtStamp;
+
+ /**
+ * The unique id of the notification this was a reply to.
+ *
+ * @var string
+ */
+ protected $inReplyTo;
+
+ /**
+ * A url to the recipient of the original (!) notification.
+ *
+ * @var string
+ */
+ protected $href;
+
+ /**
+ * The type of message, see the SharingPlugin::STATUS_ constants.
+ *
+ * @var int
+ */
+ protected $type;
+
+ /**
+ * A url to the shared calendar.
+ *
+ * @var string
+ */
+ protected $hostUrl;
+
+ /**
+ * A description of the share request
+ *
+ * @var string
+ */
+ protected $summary;
+
+ /**
+ * Notification Etag
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * Creates the Invite Reply Notification.
+ *
+ * This constructor receives an array with the following elements:
+ *
+ * * id - A unique id
+ * * etag - The etag
+ * * dtStamp - A DateTime object with a timestamp for the notification.
+ * * inReplyTo - This should refer to the 'id' of the notification
+ * this is a reply to.
+ * * type - The type of notification, see SharingPlugin::STATUS_*
+ * constants for details.
+ * * hostUrl - A url to the shared calendar.
+ * * summary - Description of the share, can be the same as the
+ * calendar, but may also be modified (optional).
+ */
+ public function __construct(array $values) {
+
+ $required = array(
+ 'id',
+ 'etag',
+ 'href',
+ 'dtStamp',
+ 'inReplyTo',
+ 'type',
+ 'hostUrl',
+ );
+ foreach($required as $item) {
+ if (!isset($values[$item])) {
+ throw new \InvalidArgumentException($item . ' is a required constructor option');
+ }
+ }
+
+ foreach($values as $key=>$value) {
+ if (!property_exists($this, $key)) {
+ throw new \InvalidArgumentException('Unknown option: ' . $key);
+ }
+ $this->$key = $value;
+ }
+
+ }
+
+ /**
+ * Serializes the notification as a single property.
+ *
+ * You should usually just encode the single top-level element of the
+ * notification.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $node) {
+
+ $prop = $node->ownerDocument->createElement('cs:invite-reply');
+ $node->appendChild($prop);
+
+ }
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serializeBody(DAV\Server $server, \DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $dt = $doc->createElement('cs:dtstamp');
+ $this->dtStamp->setTimezone(new \DateTimezone('GMT'));
+ $dt->appendChild($doc->createTextNode($this->dtStamp->format('Ymd\\THis\\Z')));
+ $node->appendChild($dt);
+
+ $prop = $doc->createElement('cs:invite-reply');
+ $node->appendChild($prop);
+
+ $uid = $doc->createElement('cs:uid');
+ $uid->appendChild($doc->createTextNode($this->id));
+ $prop->appendChild($uid);
+
+ $inReplyTo = $doc->createElement('cs:in-reply-to');
+ $inReplyTo->appendChild( $doc->createTextNode($this->inReplyTo) );
+ $prop->appendChild($inReplyTo);
+
+ $href = $doc->createElement('d:href');
+ $href->appendChild( $doc->createTextNode($this->href) );
+ $prop->appendChild($href);
+
+ $nodeName = null;
+ switch($this->type) {
+
+ case SharingPlugin::STATUS_ACCEPTED :
+ $nodeName = 'cs:invite-accepted';
+ break;
+ case SharingPlugin::STATUS_DECLINED :
+ $nodeName = 'cs:invite-declined';
+ break;
+
+ }
+ $prop->appendChild(
+ $doc->createElement($nodeName)
+ );
+ $hostHref = $doc->createElement('d:href', $server->getBaseUri() . $this->hostUrl);
+ $hostUrl = $doc->createElement('cs:hosturl');
+ $hostUrl->appendChild($hostHref);
+ $prop->appendChild($hostUrl);
+
+ if ($this->summary) {
+ $summary = $doc->createElement('cs:summary');
+ $summary->appendChild($doc->createTextNode($this->summary));
+ $prop->appendChild($summary);
+ }
+
+ }
+
+ /**
+ * Returns a unique id for this notification
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ public function getId() {
+
+ return $this->id;
+
+ }
+
+ /**
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ return $this->etag;
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php
new file mode 100644
index 0000000..aef0ea8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Notifications/Notification/SystemStatus.php
@@ -0,0 +1,182 @@
+<?php
+
+namespace OldSabre\CalDAV\Notifications\Notification;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * SystemStatus notification
+ *
+ * This notification can be used to indicate to the user that the system is
+ * down.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SystemStatus extends DAV\Property implements CalDAV\Notifications\INotificationType {
+
+ const TYPE_LOW = 1;
+ const TYPE_MEDIUM = 2;
+ const TYPE_HIGH = 3;
+
+ /**
+ * A unique id
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * The type of alert. This should be one of the TYPE_ constants.
+ *
+ * @var int
+ */
+ protected $type;
+
+ /**
+ * A human-readable description of the problem.
+ *
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * A url to a website with more information for the user.
+ *
+ * @var string
+ */
+ protected $href;
+
+ /**
+ * Notification Etag
+ *
+ * @var string
+ */
+ protected $etag;
+
+ /**
+ * Creates the notification.
+ *
+ * Some kind of unique id should be provided. This is used to generate a
+ * url.
+ *
+ * @param string $id
+ * @param string $etag
+ * @param int $type
+ * @param string $description
+ * @param string $href
+ */
+ public function __construct($id, $etag, $type = self::TYPE_HIGH, $description = null, $href = null) {
+
+ $this->id = $id;
+ $this->type = $type;
+ $this->description = $description;
+ $this->href = $href;
+ $this->etag = $etag;
+
+ }
+
+ /**
+ * Serializes the notification as a single property.
+ *
+ * You should usually just encode the single top-level element of the
+ * notification.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $node) {
+
+ switch($this->type) {
+ case self::TYPE_LOW :
+ $type = 'low';
+ break;
+ case self::TYPE_MEDIUM :
+ $type = 'medium';
+ break;
+ default :
+ case self::TYPE_HIGH :
+ $type = 'high';
+ break;
+ }
+
+ $prop = $node->ownerDocument->createElement('cs:systemstatus');
+ $prop->setAttribute('type', $type);
+
+ $node->appendChild($prop);
+
+ }
+
+ /**
+ * This method serializes the entire notification, as it is used in the
+ * response body.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serializeBody(DAV\Server $server, \DOMElement $node) {
+
+ switch($this->type) {
+ case self::TYPE_LOW :
+ $type = 'low';
+ break;
+ case self::TYPE_MEDIUM :
+ $type = 'medium';
+ break;
+ default :
+ case self::TYPE_HIGH :
+ $type = 'high';
+ break;
+ }
+
+ $prop = $node->ownerDocument->createElement('cs:systemstatus');
+ $prop->setAttribute('type', $type);
+
+ if ($this->description) {
+ $text = $node->ownerDocument->createTextNode($this->description);
+ $desc = $node->ownerDocument->createElement('cs:description');
+ $desc->appendChild($text);
+ $prop->appendChild($desc);
+ }
+ if ($this->href) {
+ $text = $node->ownerDocument->createTextNode($this->href);
+ $href = $node->ownerDocument->createElement('d:href');
+ $href->appendChild($text);
+ $prop->appendChild($href);
+ }
+
+ $node->appendChild($prop);
+
+ }
+
+ /**
+ * Returns a unique id for this notification
+ *
+ * This is just the base url. This should generally be some kind of unique
+ * id.
+ *
+ * @return string
+ */
+ public function getId() {
+
+ return $this->id;
+
+ }
+
+ /*
+ * Returns the ETag for this notification.
+ *
+ * The ETag must be surrounded by literal double-quotes.
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ return $this->etag;
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php
new file mode 100644
index 0000000..8d9a279
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Plugin.php
@@ -0,0 +1,1338 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+use OldSabre\VObject;
+
+/**
+ * CalDAV plugin
+ *
+ * This plugin provides functionality added by CalDAV (RFC 4791)
+ * It implements new reports, and the MKCALENDAR method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * This is the official CalDAV namespace
+ */
+ const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
+
+ /**
+ * This is the namespace for the proprietary calendarserver extensions
+ */
+ const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
+
+ /**
+ * The hardcoded root for calendar objects. It is unfortunate
+ * that we're stuck with it, but it will have to do for now
+ */
+ const CALENDAR_ROOT = 'calendars';
+
+ /**
+ * Reference to server object
+ *
+ * @var DAV\Server
+ */
+ protected $server;
+
+ /**
+ * The email handler for invites and other scheduling messages.
+ *
+ * @var Schedule\IMip
+ */
+ protected $imipHandler;
+
+ /**
+ * Sets the iMIP handler.
+ *
+ * iMIP = The email transport of iCalendar scheduling messages. Setting
+ * this is optional, but if you want the server to allow invites to be sent
+ * out, you must set a handler.
+ *
+ * Specifically iCal will plain assume that the server supports this. If
+ * the server doesn't, iCal will display errors when inviting people to
+ * events.
+ *
+ * @param Schedule\IMip $imipHandler
+ * @return void
+ */
+ public function setIMipHandler(Schedule\IMip $imipHandler) {
+
+ $this->imipHandler = $imipHandler;
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ // The MKCALENDAR is only available on unmapped uri's, whose
+ // parents extend IExtendedCollection
+ list($parent, $name) = DAV\URLUtil::splitPath($uri);
+
+ $node = $this->server->tree->getNodeForPath($parent);
+
+ if ($node instanceof DAV\IExtendedCollection) {
+ try {
+ $node->getChild($name);
+ } catch (DAV\Exception\NotFound $e) {
+ return array('MKCALENDAR');
+ }
+ }
+ return array();
+
+ }
+
+ /**
+ * Returns a list of features for the DAV: HTTP header.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('calendar-access', 'calendar-proxy');
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'caldav';
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ $reports = array();
+ if ($node instanceof ICalendar || $node instanceof ICalendarObject) {
+ $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
+ $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
+ }
+ if ($node instanceof ICalendar) {
+ $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
+ }
+ return $reports;
+
+ }
+
+ /**
+ * Initializes the plugin
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+
+ $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
+ //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
+ $server->subscribeEvent('report',array($this,'report'));
+ $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
+ $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
+ $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
+ $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
+ $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
+ $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'));
+
+ $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
+ $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
+
+ $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'OldSabre\\CalDAV\\Property\\SupportedCalendarComponentSet';
+ $server->propertyMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'OldSabre\\CalDAV\\Property\\ScheduleCalendarTransp';
+
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Schedule\\IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
+ $server->resourceTypeMapping['\\OldSabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
+
+ array_push($server->protectedProperties,
+
+ '{' . self::NS_CALDAV . '}supported-calendar-component-set',
+ '{' . self::NS_CALDAV . '}supported-calendar-data',
+ '{' . self::NS_CALDAV . '}max-resource-size',
+ '{' . self::NS_CALDAV . '}min-date-time',
+ '{' . self::NS_CALDAV . '}max-date-time',
+ '{' . self::NS_CALDAV . '}max-instances',
+ '{' . self::NS_CALDAV . '}max-attendees-per-instance',
+ '{' . self::NS_CALDAV . '}calendar-home-set',
+ '{' . self::NS_CALDAV . '}supported-collation-set',
+ '{' . self::NS_CALDAV . '}calendar-data',
+
+ // scheduling extension
+ '{' . self::NS_CALDAV . '}schedule-inbox-URL',
+ '{' . self::NS_CALDAV . '}schedule-outbox-URL',
+ '{' . self::NS_CALDAV . '}calendar-user-address-set',
+ '{' . self::NS_CALDAV . '}calendar-user-type',
+
+ // CalendarServer extensions
+ '{' . self::NS_CALENDARSERVER . '}getctag',
+ '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
+ '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
+ '{' . self::NS_CALENDARSERVER . '}notification-URL',
+ '{' . self::NS_CALENDARSERVER . '}notificationtype'
+
+ );
+ }
+
+ /**
+ * This function handles support for the MKCALENDAR method
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ switch ($method) {
+ case 'MKCALENDAR' :
+ $this->httpMkCalendar($uri);
+ // false is returned to stop the propagation of the
+ // unknownMethod event.
+ return false;
+ case 'POST' :
+
+ // Checking if this is a text/calendar content type
+ $contentType = $this->server->httpRequest->getHeader('Content-Type');
+ if (strpos($contentType, 'text/calendar')!==0) {
+ return;
+ }
+
+ // Checking if we're talking to an outbox
+ try {
+ $node = $this->server->tree->getNodeForPath($uri);
+ } catch (DAV\Exception\NotFound $e) {
+ return;
+ }
+ if (!$node instanceof Schedule\IOutbox)
+ return;
+
+ $this->outboxRequest($node, $uri);
+ return false;
+
+ }
+
+ }
+
+ /**
+ * This functions handles REPORT requests specific to CalDAV
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @return bool
+ */
+ public function report($reportName,$dom) {
+
+ switch($reportName) {
+ case '{'.self::NS_CALDAV.'}calendar-multiget' :
+ $this->calendarMultiGetReport($dom);
+ return false;
+ case '{'.self::NS_CALDAV.'}calendar-query' :
+ $this->calendarQueryReport($dom);
+ return false;
+ case '{'.self::NS_CALDAV.'}free-busy-query' :
+ $this->freeBusyQueryReport($dom);
+ return false;
+
+ }
+
+
+ }
+
+ /**
+ * This function handles the MKCALENDAR HTTP method, which creates
+ * a new calendar.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function httpMkCalendar($uri) {
+
+ // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
+ // for clients matching iCal in the user agent
+ //$ua = $this->server->httpRequest->getHeader('User-Agent');
+ //if (strpos($ua,'iCal/')!==false) {
+ // throw new \OldSabre\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.');
+ //}
+
+ $body = $this->server->httpRequest->getBody(true);
+ $properties = array();
+
+ if ($body) {
+
+ $dom = DAV\XMLUtil::loadDOMDocument($body);
+
+ foreach($dom->firstChild->childNodes as $child) {
+
+ if (DAV\XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
+ foreach(DAV\XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
+ $properties[$k] = $prop;
+ }
+
+ }
+ }
+
+ $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
+
+ $this->server->createCollection($uri,$resourceType,$properties);
+
+ $this->server->httpResponse->sendStatus(201);
+ $this->server->httpResponse->setHeader('Content-Length',0);
+ }
+
+ /**
+ * beforeGetProperties
+ *
+ * This method handler is invoked before any after properties for a
+ * resource are fetched. This allows us to add in any CalDAV specific
+ * properties.
+ *
+ * @param string $path
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
+
+ if ($node instanceof DAVACL\IPrincipal) {
+
+ // calendar-home-set property
+ $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
+ if (in_array($calHome,$requestedProperties)) {
+ $principalId = $node->getName();
+ $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/';
+
+ unset($requestedProperties[array_search($calHome, $requestedProperties)]);
+ $returnedProperties[200][$calHome] = new DAV\Property\Href($calendarHomePath);
+
+ }
+
+ // schedule-outbox-URL property
+ $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
+ if (in_array($scheduleProp,$requestedProperties)) {
+ $principalId = $node->getName();
+ $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox';
+
+ unset($requestedProperties[array_search($scheduleProp, $requestedProperties)]);
+ $returnedProperties[200][$scheduleProp] = new DAV\Property\Href($outboxPath);
+
+ }
+
+ // calendar-user-address-set property
+ $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
+ if (in_array($calProp,$requestedProperties)) {
+
+ $addresses = $node->getAlternateUriSet();
+ $addresses[] = $this->server->getBaseUri() . DAV\URLUtil::encodePath($node->getPrincipalUrl() . '/');
+ unset($requestedProperties[array_search($calProp, $requestedProperties)]);
+ $returnedProperties[200][$calProp] = new DAV\Property\HrefList($addresses, false);
+
+ }
+
+ // These two properties are shortcuts for ical to easily find
+ // other principals this principal has access to.
+ $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
+ $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
+ if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
+
+ $aclPlugin = $this->server->getPlugin('acl');
+ $membership = $aclPlugin->getPrincipalMembership($path);
+ $readList = array();
+ $writeList = array();
+
+ foreach($membership as $group) {
+
+ $groupNode = $this->server->tree->getNodeForPath($group);
+
+ // If the node is either ap proxy-read or proxy-write
+ // group, we grab the parent principal and add it to the
+ // list.
+ if ($groupNode instanceof Principal\IProxyRead) {
+ list($readList[]) = DAV\URLUtil::splitPath($group);
+ }
+ if ($groupNode instanceof Principal\IProxyWrite) {
+ list($writeList[]) = DAV\URLUtil::splitPath($group);
+ }
+
+ }
+ if (in_array($propRead,$requestedProperties)) {
+ unset($requestedProperties[$propRead]);
+ $returnedProperties[200][$propRead] = new DAV\Property\HrefList($readList);
+ }
+ if (in_array($propWrite,$requestedProperties)) {
+ unset($requestedProperties[$propWrite]);
+ $returnedProperties[200][$propWrite] = new DAV\Property\HrefList($writeList);
+ }
+
+ }
+
+ // notification-URL property
+ $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL';
+ if (($index = array_search($notificationUrl, $requestedProperties)) !== false) {
+ $principalId = $node->getName();
+ $calendarHomePath = 'calendars/' . $principalId . '/notifications/';
+ unset($requestedProperties[$index]);
+ $returnedProperties[200][$notificationUrl] = new DAV\Property\Href($calendarHomePath);
+ }
+
+ } // instanceof IPrincipal
+
+ if ($node instanceof Notifications\INode) {
+
+ $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
+ if (($index = array_search($propertyName, $requestedProperties)) !== false) {
+
+ $returnedProperties[200][$propertyName] =
+ $node->getNotificationType();
+
+ unset($requestedProperties[$index]);
+
+ }
+
+ } // instanceof Notifications_INode
+
+
+ if ($node instanceof ICalendarObject) {
+ // The calendar-data property is not supposed to be a 'real'
+ // property, but in large chunks of the spec it does act as such.
+ // Therefore we simply expose it as a property.
+ $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
+ if (in_array($calDataProp, $requestedProperties)) {
+ unset($requestedProperties[$calDataProp]);
+ $val = $node->get();
+ if (is_resource($val))
+ $val = stream_get_contents($val);
+
+ // Taking out \r to not screw up the xml output
+ $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
+
+ }
+ }
+
+ }
+
+ /**
+ * This function handles the calendar-multiget REPORT.
+ *
+ * This report is used by the client to fetch the content of a series
+ * of urls. Effectively avoiding a lot of redundant requests.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ public function calendarMultiGetReport($dom) {
+
+ $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
+ $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
+
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
+ $xpath->registerNameSpace('dav','urn:DAV');
+
+ $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
+ if ($expand->length>0) {
+ $expandElem = $expand->item(0);
+ $start = $expandElem->getAttribute('start');
+ $end = $expandElem->getAttribute('end');
+ if(!$start || !$end) {
+ throw new DAV\Exception\BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
+ }
+ $start = VObject\DateTimeParser::parseDateTime($start);
+ $end = VObject\DateTimeParser::parseDateTime($end);
+
+ if ($end <= $start) {
+ throw new DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
+ }
+
+ $expand = true;
+
+ } else {
+
+ $expand = false;
+
+ }
+
+ foreach($hrefElems as $elem) {
+ $uri = $this->server->calculateUri($elem->nodeValue);
+ list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
+
+ if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
+ $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
+ $vObject->expand($start, $end);
+ $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+ }
+
+ $propertyList[]=$objProps;
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
+ $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * This function handles the calendar-query REPORT
+ *
+ * This report is used by clients to request calendar objects based on
+ * complex conditions.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ public function calendarQueryReport($dom) {
+
+ $parser = new CalendarQueryParser($dom);
+ $parser->parse();
+
+ $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
+ $depth = $this->server->getHTTPDepth(0);
+
+ // The default result is an empty array
+ $result = array();
+
+ // The calendarobject was requested directly. In this case we handle
+ // this locally.
+ if ($depth == 0 && $node instanceof ICalendarObject) {
+
+ $requestedCalendarData = true;
+ $requestedProperties = $parser->requestedProperties;
+
+ if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
+
+ // We always retrieve calendar-data, as we need it for filtering.
+ $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
+
+ // If calendar-data wasn't explicitly requested, we need to remove
+ // it after processing.
+ $requestedCalendarData = false;
+ }
+
+ $properties = $this->server->getPropertiesForPath(
+ $this->server->getRequestUri(),
+ $requestedProperties,
+ 0
+ );
+
+ // This array should have only 1 element, the first calendar
+ // object.
+ $properties = current($properties);
+
+ // If there wasn't any calendar-data returned somehow, we ignore
+ // this.
+ if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
+
+ $validator = new CalendarQueryValidator();
+
+ $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+ if ($validator->validate($vObject,$parser->filters)) {
+
+ // If the client didn't require the calendar-data property,
+ // we won't give it back.
+ if (!$requestedCalendarData) {
+ unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+ } else {
+ if ($parser->expand) {
+ $vObject->expand($parser->expand['start'], $parser->expand['end']);
+ $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+ }
+ }
+
+ $result = array($properties);
+
+ }
+
+ }
+
+ }
+ // If we're dealing with a calendar, the calendar itself is responsible
+ // for the calendar-query.
+ if ($node instanceof ICalendar && $depth = 1) {
+
+ $nodePaths = $node->calendarQuery($parser->filters);
+
+ foreach($nodePaths as $path) {
+
+ list($properties) =
+ $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
+
+ if ($parser->expand) {
+ // We need to do some post-processing
+ $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+ $vObject->expand($parser->expand['start'], $parser->expand['end']);
+ $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+ }
+
+ $result[] = $properties;
+
+ }
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
+ $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * This method is responsible for parsing the request and generating the
+ * response for the CALDAV:free-busy-query REPORT.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ protected function freeBusyQueryReport(\DOMNode $dom) {
+
+ $start = null;
+ $end = null;
+
+ foreach($dom->firstChild->childNodes as $childNode) {
+
+ $clark = DAV\XMLUtil::toClarkNotation($childNode);
+ if ($clark == '{' . self::NS_CALDAV . '}time-range') {
+ $start = $childNode->getAttribute('start');
+ $end = $childNode->getAttribute('end');
+ break;
+ }
+
+ }
+ if ($start) {
+ $start = VObject\DateTimeParser::parseDateTime($start);
+ }
+ if ($end) {
+ $end = VObject\DateTimeParser::parseDateTime($end);
+ }
+
+ if (!$start && !$end) {
+ throw new DAV\Exception\BadRequest('The freebusy report must have a time-range filter');
+ }
+ $acl = $this->server->getPlugin('acl');
+
+ if (!$acl) {
+ throw new DAV\Exception('The ACL plugin must be loaded for free-busy queries to work');
+ }
+ $uri = $this->server->getRequestUri();
+ $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
+
+ $calendar = $this->server->tree->getNodeForPath($uri);
+ if (!$calendar instanceof ICalendar) {
+ throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
+ }
+
+ // Doing a calendar-query first, to make sure we get the most
+ // performance.
+ $urls = $calendar->calendarQuery(array(
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => array(
+ array(
+ 'name' => 'VEVENT',
+ 'comp-filters' => array(),
+ 'prop-filters' => array(),
+ 'is-not-defined' => false,
+ 'time-range' => array(
+ 'start' => $start,
+ 'end' => $end,
+ ),
+ ),
+ ),
+ 'prop-filters' => array(),
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ));
+
+ $objects = array_map(function($url) use ($calendar) {
+ $obj = $calendar->getChild($url)->get();
+ return $obj;
+ }, $urls);
+
+ $generator = new VObject\FreeBusyGenerator();
+ $generator->setObjects($objects);
+ $generator->setTimeRange($start, $end);
+ $result = $generator->getResult();
+ $result = $result->serialize();
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
+ $this->server->httpResponse->setHeader('Content-Length', strlen($result));
+ $this->server->httpResponse->sendBody($result);
+
+ }
+
+ /**
+ * This method is triggered before a file gets updated with new content.
+ *
+ * This plugin uses this method to ensure that CalDAV objects receive
+ * valid calendar data.
+ *
+ * @param string $path
+ * @param DAV\IFile $node
+ * @param resource $data
+ * @return void
+ */
+ public function beforeWriteContent($path, DAV\IFile $node, &$data) {
+
+ if (!$node instanceof ICalendarObject)
+ return;
+
+ $this->validateICalendar($data, $path);
+
+ }
+
+ /**
+ * This method is triggered before a new file is created.
+ *
+ * This plugin uses this method to ensure that newly created calendar
+ * objects contain valid calendar data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param DAV\ICollection $parentNode
+ * @return void
+ */
+ public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode) {
+
+ if (!$parentNode instanceof Calendar)
+ return;
+
+ $this->validateICalendar($data, $path);
+
+ }
+
+ /**
+ * This event is triggered before any HTTP request is handled.
+ *
+ * We use this to intercept GET calls to notification nodes, and return the
+ * proper response.
+ *
+ * @param string $method
+ * @param string $path
+ * @return void
+ */
+ public function beforeMethod($method, $path) {
+
+ if ($method!=='GET') return;
+
+ try {
+ $node = $this->server->tree->getNodeForPath($path);
+ } catch (DAV\Exception\NotFound $e) {
+ return;
+ }
+
+ if (!$node instanceof Notifications\INode)
+ return;
+
+ if (!$this->server->checkPreconditions(true)) return false;
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+
+ $dom->formatOutput = true;
+
+ $root = $dom->createElement('cs:notification');
+ foreach($this->server->xmlNamespaces as $namespace => $prefix) {
+ $root->setAttribute('xmlns:' . $prefix, $namespace);
+ }
+
+ $dom->appendChild($root);
+ $node->getNotificationType()->serializeBody($this->server, $root);
+
+ $this->server->httpResponse->setHeader('Content-Type','application/xml');
+ $this->server->httpResponse->setHeader('ETag',$node->getETag());
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->sendBody($dom->saveXML());
+
+ return false;
+
+ }
+
+ /**
+ * Checks if the submitted iCalendar data is in fact, valid.
+ *
+ * An exception is thrown if it's not.
+ *
+ * @param resource|string $data
+ * @param string $path
+ * @return void
+ */
+ protected function validateICalendar(&$data, $path) {
+
+ // If it's a stream, we convert it to a string first.
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ // Converting the data to unicode, if needed.
+ $data = DAV\StringUtil::ensureUTF8($data);
+
+ try {
+
+ $vobj = VObject\Reader::read($data);
+
+ } catch (VObject\ParseException $e) {
+
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
+
+ }
+
+ if ($vobj->name !== 'VCALENDAR') {
+ throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
+ }
+
+ // Get the Supported Components for the target calendar
+ list($parentPath,$object) = DAV\URLUtil::splitPath($path);
+ $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
+ $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
+
+ $foundType = null;
+ $foundUID = null;
+ foreach($vobj->getComponents() as $component) {
+ switch($component->name) {
+ case 'VTIMEZONE' :
+ continue 2;
+ case 'VEVENT' :
+ case 'VTODO' :
+ case 'VJOURNAL' :
+ if (is_null($foundType)) {
+ $foundType = $component->name;
+ if (!in_array($foundType, $supportedComponents)) {
+ throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
+ }
+ if (!isset($component->UID)) {
+ throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
+ }
+ $foundUID = (string)$component->UID;
+ } else {
+ if ($foundType !== $component->name) {
+ throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
+ }
+ if ($foundUID !== (string)$component->UID) {
+ throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
+ }
+ }
+ break;
+ default :
+ throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
+
+ }
+ }
+ if (!$foundType)
+ throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
+
+ }
+
+ /**
+ * This method handles POST requests to the schedule-outbox.
+ *
+ * Currently, two types of requests are support:
+ * * FREEBUSY requests from RFC 6638
+ * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
+ *
+ * The latter is from an expired early draft of the CalDAV scheduling
+ * extensions, but iCal depends on a feature from that spec, so we
+ * implement it.
+ *
+ * @param Schedule\IOutbox $outboxNode
+ * @param string $outboxUri
+ * @return void
+ */
+ public function outboxRequest(Schedule\IOutbox $outboxNode, $outboxUri) {
+
+ // Parsing the request body
+ try {
+ $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
+ } catch (VObject\ParseException $e) {
+ throw new DAV\Exception\BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
+ }
+
+ // The incoming iCalendar object must have a METHOD property, and a
+ // component. The combination of both determines what type of request
+ // this is.
+ $componentType = null;
+ foreach($vObject->getComponents() as $component) {
+ if ($component->name !== 'VTIMEZONE') {
+ $componentType = $component->name;
+ break;
+ }
+ }
+ if (is_null($componentType)) {
+ throw new DAV\Exception\BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
+ }
+
+ // Validating the METHOD
+ $method = strtoupper((string)$vObject->METHOD);
+ if (!$method) {
+ throw new DAV\Exception\BadRequest('A METHOD property must be specified in iTIP messages');
+ }
+
+ // So we support two types of requests:
+ //
+ // REQUEST with a VFREEBUSY component
+ // REQUEST, REPLY, ADD, CANCEL on VEVENT components
+
+ $acl = $this->server->getPlugin('acl');
+
+ if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
+
+ $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-query-freebusy');
+ $this->handleFreeBusyRequest($outboxNode, $vObject);
+
+ } elseif ($componentType === 'VEVENT' && in_array($method, array('REQUEST','REPLY','ADD','CANCEL'))) {
+
+ $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
+ $this->handleEventNotification($outboxNode, $vObject);
+
+ } else {
+
+ throw new DAV\Exception\NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)');
+
+ }
+
+ }
+
+ /**
+ * This method handles the REQUEST, REPLY, ADD and CANCEL methods for
+ * VEVENT iTip messages.
+ *
+ * @return void
+ */
+ protected function handleEventNotification(Schedule\IOutbox $outboxNode, VObject\Component $vObject) {
+
+ $originator = $this->server->httpRequest->getHeader('Originator');
+ $recipients = $this->server->httpRequest->getHeader('Recipient');
+
+ if (!$originator) {
+ throw new DAV\Exception\BadRequest('The Originator: header must be specified when making POST requests');
+ }
+ if (!$recipients) {
+ throw new DAV\Exception\BadRequest('The Recipient: header must be specified when making POST requests');
+ }
+
+ $recipients = explode(',',$recipients);
+ foreach($recipients as $k=>$recipient) {
+
+ $recipient = trim($recipient);
+ if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
+ throw new DAV\Exception\BadRequest('Recipients must start with mailto: and must be valid email address');
+ }
+ $recipient = substr($recipient, 7);
+ $recipients[$k] = $recipient;
+ }
+
+ // We need to make sure that 'originator' matches one of the email
+ // addresses of the selected principal.
+ $principal = $outboxNode->getOwner();
+ $props = $this->server->getProperties($principal,array(
+ '{' . self::NS_CALDAV . '}calendar-user-address-set',
+ ));
+
+ $addresses = array();
+ if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
+ $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
+ }
+
+ $found = false;
+ foreach($addresses as $address) {
+
+ // Trimming the / on both sides, just in case..
+ if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
+ $found = true;
+ break;
+ }
+
+ }
+
+ if (!$found) {
+ throw new DAV\Exception\Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
+ }
+
+ // If the Originator header was a url, and not a mailto: address..
+ // we're going to try to pull the mailto: from the vobject body.
+ if (strtolower(substr($originator,0,7)) !== 'mailto:') {
+ $originator = (string)$vObject->VEVENT->ORGANIZER;
+
+ }
+ if (strtolower(substr($originator,0,7)) !== 'mailto:') {
+ throw new DAV\Exception\Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
+ }
+ $originator = substr($originator,7);
+
+ $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml');
+ $this->server->httpResponse->sendBody($this->generateScheduleResponse($result));
+
+ }
+
+ /**
+ * Sends an iMIP message by email.
+ *
+ * This method must return an array with status codes per recipient.
+ * This should look something like:
+ *
+ * array(
+ * 'user1@example.org' => '2.0;Success'
+ * )
+ *
+ * Formatting for this status code can be found at:
+ * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
+ *
+ * A list of valid status codes can be found at:
+ * https://tools.ietf.org/html/rfc5546#section-3.6
+ *
+ * @param string $originator
+ * @param array $recipients
+ * @param VObject\Component $vObject
+ * @param string $principal Principal url
+ * @return array
+ */
+ protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
+
+ if (!$this->imipHandler) {
+ $resultStatus = '5.2;This server does not support this operation';
+ } else {
+ $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
+ $resultStatus = '2.0;Success';
+ }
+
+ $result = array();
+ foreach($recipients as $recipient) {
+ $result[$recipient] = $resultStatus;
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Generates a schedule-response XML body
+ *
+ * The recipients array is a key->value list, containing email addresses
+ * and iTip status codes. See the iMIPMessage method for a description of
+ * the value.
+ *
+ * @param array $recipients
+ * @return string
+ */
+ public function generateScheduleResponse(array $recipients) {
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $xscheduleResponse = $dom->createElement('cal:schedule-response');
+ $dom->appendChild($xscheduleResponse);
+
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
+
+ }
+
+ foreach($recipients as $recipient=>$status) {
+ $xresponse = $dom->createElement('cal:response');
+
+ $xrecipient = $dom->createElement('cal:recipient');
+ $xrecipient->appendChild($dom->createTextNode($recipient));
+ $xresponse->appendChild($xrecipient);
+
+ $xrequestStatus = $dom->createElement('cal:request-status');
+ $xrequestStatus->appendChild($dom->createTextNode($status));
+ $xresponse->appendChild($xrequestStatus);
+
+ $xscheduleResponse->appendChild($xresponse);
+
+ }
+
+ return $dom->saveXML();
+
+ }
+
+ /**
+ * This method is responsible for parsing a free-busy query request and
+ * returning it's result.
+ *
+ * @param Schedule\IOutbox $outbox
+ * @param string $request
+ * @return string
+ */
+ protected function handleFreeBusyRequest(Schedule\IOutbox $outbox, VObject\Component $vObject) {
+
+ $vFreeBusy = $vObject->VFREEBUSY;
+ $organizer = $vFreeBusy->organizer;
+
+ $organizer = (string)$organizer;
+
+ // Validating if the organizer matches the owner of the inbox.
+ $owner = $outbox->getOwner();
+
+ $caldavNS = '{' . Plugin::NS_CALDAV . '}';
+
+ $uas = $caldavNS . 'calendar-user-address-set';
+ $props = $this->server->getProperties($owner,array($uas));
+
+ if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
+ throw new DAV\Exception\Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
+ }
+
+ if (!isset($vFreeBusy->ATTENDEE)) {
+ throw new DAV\Exception\BadRequest('You must at least specify 1 attendee');
+ }
+
+ $attendees = array();
+ foreach($vFreeBusy->ATTENDEE as $attendee) {
+ $attendees[]= (string)$attendee;
+ }
+
+
+ if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
+ throw new DAV\Exception\BadRequest('DTSTART and DTEND must both be specified');
+ }
+
+ $startRange = $vFreeBusy->DTSTART->getDateTime();
+ $endRange = $vFreeBusy->DTEND->getDateTime();
+
+ $results = array();
+ foreach($attendees as $attendee) {
+ $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
+ }
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $scheduleResponse = $dom->createElement('cal:schedule-response');
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+ $dom->appendChild($scheduleResponse);
+
+ foreach($results as $result) {
+ $response = $dom->createElement('cal:response');
+
+ $recipient = $dom->createElement('cal:recipient');
+ $recipientHref = $dom->createElement('d:href');
+
+ $recipientHref->appendChild($dom->createTextNode($result['href']));
+ $recipient->appendChild($recipientHref);
+ $response->appendChild($recipient);
+
+ $reqStatus = $dom->createElement('cal:request-status');
+ $reqStatus->appendChild($dom->createTextNode($result['request-status']));
+ $response->appendChild($reqStatus);
+
+ if (isset($result['calendar-data'])) {
+
+ $calendardata = $dom->createElement('cal:calendar-data');
+ $calendardata->appendChild($dom->createTextNode(str_replace("\r\n","\n",$result['calendar-data']->serialize())));
+ $response->appendChild($calendardata);
+
+ }
+ $scheduleResponse->appendChild($response);
+ }
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml');
+ $this->server->httpResponse->sendBody($dom->saveXML());
+
+ }
+
+ /**
+ * Returns free-busy information for a specific address. The returned
+ * data is an array containing the following properties:
+ *
+ * calendar-data : A VFREEBUSY VObject
+ * request-status : an iTip status code.
+ * href: The principal's email address, as requested
+ *
+ * The following request status codes may be returned:
+ * * 2.0;description
+ * * 3.7;description
+ *
+ * @param string $email address
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @param VObject\Component $request
+ * @return array
+ */
+ protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
+
+ $caldavNS = '{' . Plugin::NS_CALDAV . '}';
+
+ $aclPlugin = $this->server->getPlugin('acl');
+ if (substr($email,0,7)==='mailto:') $email = substr($email,7);
+
+ $result = $aclPlugin->principalSearch(
+ array('{http://sabredav.org/ns}email-address' => $email),
+ array(
+ '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
+ '{http://sabredav.org/ns}email-address',
+ )
+ );
+
+ if (!count($result)) {
+ return array(
+ 'request-status' => '3.7;Could not find principal',
+ 'href' => 'mailto:' . $email,
+ );
+ }
+
+ if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
+ return array(
+ 'request-status' => '3.7;No calendar-home-set property found',
+ 'href' => 'mailto:' . $email,
+ );
+ }
+ $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
+
+ // Grabbing the calendar list
+ $objects = array();
+ foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
+ if (!$node instanceof ICalendar) {
+ continue;
+ }
+ $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy');
+
+ // Getting the list of object uris within the time-range
+ $urls = $node->calendarQuery(array(
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => array(
+ array(
+ 'name' => 'VEVENT',
+ 'comp-filters' => array(),
+ 'prop-filters' => array(),
+ 'is-not-defined' => false,
+ 'time-range' => array(
+ 'start' => $start,
+ 'end' => $end,
+ ),
+ ),
+ ),
+ 'prop-filters' => array(),
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ));
+
+ $calObjects = array_map(function($url) use ($node) {
+ $obj = $node->getChild($url)->get();
+ return $obj;
+ }, $urls);
+
+ $objects = array_merge($objects,$calObjects);
+
+ }
+
+ $vcalendar = new VObject\Component\VCalendar();
+ $vcalendar->VERSION = '2.0';
+ $vcalendar->METHOD = 'REPLY';
+ $vcalendar->CALSCALE = 'GREGORIAN';
+ $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
+
+ $generator = new VObject\FreeBusyGenerator();
+ $generator->setObjects($objects);
+ $generator->setTimeRange($start, $end);
+ $generator->setBaseObject($vcalendar);
+
+ $result = $generator->getResult();
+
+ $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
+ $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
+ $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
+
+ return array(
+ 'calendar-data' => $result,
+ 'request-status' => '2.0;Success',
+ 'href' => 'mailto:' . $email,
+ );
+ }
+
+ /**
+ * This method is used to generate HTML output for the
+ * DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new calendars.
+ *
+ * @param DAV\INode $node
+ * @param string $output
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output) {
+
+ if (!$node instanceof UserCalendars)
+ return;
+
+ $output.= '<tr><td colspan="2"><form method="post" action="">
+ <h3>Create new calendar</h3>
+ <input type="hidden" name="sabreAction" value="mkcalendar" />
+ <label>Name (uri):</label> <input type="text" name="name" /><br />
+ <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
+ <input type="submit" value="create" />
+ </form>
+ </td></tr>';
+
+ return false;
+
+ }
+
+ /**
+ * This method allows us to intercept the 'mkcalendar' sabreAction. This
+ * action enables the user to create new calendars from the browser plugin.
+ *
+ * @param string $uri
+ * @param string $action
+ * @param array $postVars
+ * @return bool
+ */
+ public function browserPostAction($uri, $action, array $postVars) {
+
+ if ($action!=='mkcalendar')
+ return;
+
+ $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
+ $properties = array();
+ if (isset($postVars['{DAV:}displayname'])) {
+ $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
+ }
+ $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
+ return false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php
new file mode 100644
index 0000000..e78fc9f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/Collection.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAVACL;
+
+/**
+ * Principal collection
+ *
+ * This is an alternative collection to the standard ACL principal collection.
+ * This collection adds support for the calendar-proxy-read and
+ * calendar-proxy-write sub-principals, as defined by the caldav-proxy
+ * specification.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Collection extends DAVACL\AbstractPrincipalCollection {
+
+ /**
+ * Returns a child object based on principal information
+ *
+ * @param array $principalInfo
+ * @return User
+ */
+ public function getChildForPrincipal(array $principalInfo) {
+
+ return new User($this->principalBackend, $principalInfo);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php
new file mode 100644
index 0000000..5194c0b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyRead.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+
+use OldSabre\DAVACL;
+
+/**
+ * ProxyRead principal interface
+ *
+ * Any principal node implementing this interface will be picked up as a 'proxy
+ * principal group'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProxyRead extends DAVACL\IPrincipal {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php
new file mode 100644
index 0000000..d6b052e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/IProxyWrite.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+
+use OldSabre\DAVACL;
+
+/**
+ * ProxyWrite principal interface
+ *
+ * Any principal node implementing this interface will be picked up as a 'proxy
+ * principal group'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProxyWrite extends DAVACL\IPrincipal {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php
new file mode 100644
index 0000000..109f802
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyRead.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * ProxyRead principal
+ *
+ * This class represents a principal group, hosted under the main principal.
+ * This is needed to implement 'Calendar delegation' support. This class is
+ * instantiated by User.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ProxyRead implements IProxyRead {
+
+ /**
+ * Principal information from the parent principal.
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Principal backend
+ *
+ * @var DAVACL\PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * Creates the object.
+ *
+ * Note that you MUST supply the parent principal information.
+ *
+ * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+ * @param array $principalInfo
+ */
+ public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
+
+ $this->principalInfo = $principalInfo;
+ $this->principalBackend = $principalBackend;
+
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'calendar-proxy-read';
+
+ }
+
+ /**
+ * Returns the last modification time
+ *
+ * @return null
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Deletes the current node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\Forbidden('Permission denied to delete node');
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to rename file');
+
+ }
+
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet() {
+
+ return array();
+
+ }
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ public function getPrincipalUrl() {
+
+ return $this->principalInfo['uri'] . '/' . $this->getName();
+
+ }
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet() {
+
+ return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership() {
+
+ return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $principals
+ * @return void
+ */
+ public function setGroupMemberSet(array $principals) {
+
+ $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+
+ }
+
+ /**
+ * Returns the displayname
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+
+ return $this->getName();
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php
new file mode 100644
index 0000000..7295254
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/ProxyWrite.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * ProxyWrite principal
+ *
+ * This class represents a principal group, hosted under the main principal.
+ * This is needed to implement 'Calendar delegation' support. This class is
+ * instantiated by User.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ProxyWrite implements IProxyWrite {
+
+ /**
+ * Parent principal information
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Principal Backend
+ *
+ * @var DAVACL\PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * Creates the object
+ *
+ * Note that you MUST supply the parent principal information.
+ *
+ * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+ * @param array $principalInfo
+ */
+ public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
+
+ $this->principalInfo = $principalInfo;
+ $this->principalBackend = $principalBackend;
+
+ }
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'calendar-proxy-write';
+
+ }
+
+ /**
+ * Returns the last modification time
+ *
+ * @return null
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Deletes the current node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\Forbidden('Permission denied to delete node');
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to rename file');
+
+ }
+
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet() {
+
+ return array();
+
+ }
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ public function getPrincipalUrl() {
+
+ return $this->principalInfo['uri'] . '/' . $this->getName();
+
+ }
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet() {
+
+ return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership() {
+
+ return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+
+ }
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $principals
+ * @return void
+ */
+ public function setGroupMemberSet(array $principals) {
+
+ $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+
+ }
+
+ /**
+ * Returns the displayname
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+
+ return $this->getName();
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php
new file mode 100644
index 0000000..345562a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Principal/User.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace OldSabre\CalDAV\Principal;
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * CalDAV principal
+ *
+ * This is a standard user-principal for CalDAV. This principal is also a
+ * collection and returns the caldav-proxy-read and caldav-proxy-write child
+ * principals.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class User extends DAVACL\Principal implements DAV\ICollection {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * @param string $name Name of the file
+ * @param resource $data Initial payload, passed as a readable stream resource.
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function createFile($name, $data = null) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws DAV\Exception\Forbidden
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new DAV\Exception\Forbidden('Permission denied to create directory');
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * @param string $name
+ * @return DAV\INode
+ */
+ public function getChild($name) {
+
+ $principal = $this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name);
+ if (!$principal) {
+ throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
+ }
+ if ($name === 'calendar-proxy-read')
+ return new ProxyRead($this->principalBackend, $this->principalProperties);
+
+ if ($name === 'calendar-proxy-write')
+ return new ProxyWrite($this->principalBackend, $this->principalProperties);
+
+ throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren() {
+
+ $r = array();
+ if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-read')) {
+ $r[] = new ProxyRead($this->principalBackend, $this->principalProperties);
+ }
+ if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-write')) {
+ $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties);
+ }
+
+ return $r;
+
+ }
+
+ /**
+ * Returns whether or not the child node exists
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ try {
+ $this->getChild($name);
+ return true;
+ } catch (DAV\Exception\NotFound $e) {
+ return false;
+ }
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ $acl = parent::getACL();
+ $acl[] = array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalProperties['uri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ );
+ $acl[] = array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalProperties['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ );
+ return $acl;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php
new file mode 100644
index 0000000..918c3d1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/AllowedSharingModes.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * AllowedSharingModes
+ *
+ * This property encodes the 'allowed-sharing-modes' property, as defined by
+ * the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
+ * namespace.
+ *
+ * This property is a representation of the supported-calendar_component-set
+ * property in the CalDAV namespace. It simply requires an array of components,
+ * such as VEVENT, VTODO
+ *
+ * @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AllowedSharingModes extends DAV\Property {
+
+ /**
+ * Whether or not a calendar can be shared with another user
+ *
+ * @var bool
+ */
+ protected $canBeShared;
+
+ /**
+ * Whether or not the calendar can be placed on a public url.
+ *
+ * @var bool
+ */
+ protected $canBePublished;
+
+ /**
+ * Constructor
+ *
+ * @param bool $canBeShared
+ * @param bool $canBePublished
+ * @return void
+ */
+ public function __construct($canBeShared, $canBePublished) {
+
+ $this->canBeShared = $canBeShared;
+ $this->canBePublished = $canBePublished;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ if ($this->canBeShared) {
+ $xcomp = $doc->createElement('cs:can-be-shared');
+ $node->appendChild($xcomp);
+ }
+ if ($this->canBePublished) {
+ $xcomp = $doc->createElement('cs:can-be-published');
+ $node->appendChild($xcomp);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php
new file mode 100644
index 0000000..0e2f8f1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/Invite.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\CalDAV\SharingPlugin as SharingPlugin;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * Invite property
+ *
+ * This property encodes the 'invite' property, as defined by
+ * the 'caldav-sharing-02' spec, in the http://calendarserver.org/ns/
+ * namespace.
+ *
+ * @see https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing-02.txt
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Invite extends DAV\Property {
+
+ /**
+ * The list of users a calendar has been shared to.
+ *
+ * @var array
+ */
+ protected $users;
+
+ /**
+ * The organizer contains information about the person who shared the
+ * object.
+ *
+ * @var array
+ */
+ protected $organizer;
+
+ /**
+ * Creates the property.
+ *
+ * Users is an array. Each element of the array has the following
+ * properties:
+ *
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first and lastname for a user.
+ * * status - One of the SharingPlugin::STATUS_* constants.
+ * * readOnly - true or false
+ * * summary - Optional, description of the share
+ *
+ * The organizer key is optional to specify. It's only useful when a
+ * 'sharee' requests the sharing information.
+ *
+ * The organizer may have the following properties:
+ * * href - Often a mailto: address.
+ * * commonName - Optional human-readable name.
+ * * firstName - Optional first name.
+ * * lastName - Optional last name.
+ *
+ * If you wonder why these two structures are so different, I guess a
+ * valid answer is that the current spec is still a draft.
+ *
+ * @param array $users
+ */
+ public function __construct(array $users, array $organizer = null) {
+
+ $this->users = $users;
+ $this->organizer = $organizer;
+
+ }
+
+ /**
+ * Returns the list of users, as it was passed to the constructor.
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->users;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ if (!is_null($this->organizer)) {
+
+ $xorganizer = $doc->createElement('cs:organizer');
+
+ $href = $doc->createElement('d:href');
+ $href->appendChild($doc->createTextNode($this->organizer['href']));
+ $xorganizer->appendChild($href);
+
+ if (isset($this->organizer['commonName']) && $this->organizer['commonName']) {
+ $commonName = $doc->createElement('cs:common-name');
+ $commonName->appendChild($doc->createTextNode($this->organizer['commonName']));
+ $xorganizer->appendChild($commonName);
+ }
+ if (isset($this->organizer['firstName']) && $this->organizer['firstName']) {
+ $firstName = $doc->createElement('cs:first-name');
+ $firstName->appendChild($doc->createTextNode($this->organizer['firstName']));
+ $xorganizer->appendChild($firstName);
+ }
+ if (isset($this->organizer['lastName']) && $this->organizer['lastName']) {
+ $lastName = $doc->createElement('cs:last-name');
+ $lastName->appendChild($doc->createTextNode($this->organizer['lastName']));
+ $xorganizer->appendChild($lastName);
+ }
+
+ $node->appendChild($xorganizer);
+
+
+ }
+
+ foreach($this->users as $user) {
+
+ $xuser = $doc->createElement('cs:user');
+
+ $href = $doc->createElement('d:href');
+ $href->appendChild($doc->createTextNode($user['href']));
+ $xuser->appendChild($href);
+
+ if (isset($user['commonName']) && $user['commonName']) {
+ $commonName = $doc->createElement('cs:common-name');
+ $commonName->appendChild($doc->createTextNode($user['commonName']));
+ $xuser->appendChild($commonName);
+ }
+
+ switch($user['status']) {
+
+ case SharingPlugin::STATUS_ACCEPTED :
+ $status = $doc->createElement('cs:invite-accepted');
+ $xuser->appendChild($status);
+ break;
+ case SharingPlugin::STATUS_DECLINED :
+ $status = $doc->createElement('cs:invite-declined');
+ $xuser->appendChild($status);
+ break;
+ case SharingPlugin::STATUS_NORESPONSE :
+ $status = $doc->createElement('cs:invite-noresponse');
+ $xuser->appendChild($status);
+ break;
+ case SharingPlugin::STATUS_INVALID :
+ $status = $doc->createElement('cs:invite-invalid');
+ $xuser->appendChild($status);
+ break;
+
+ }
+
+ $xaccess = $doc->createElement('cs:access');
+
+ if ($user['readOnly']) {
+ $xaccess->appendChild(
+ $doc->createElement('cs:read')
+ );
+ } else {
+ $xaccess->appendChild(
+ $doc->createElement('cs:read-write')
+ );
+ }
+ $xuser->appendChild($xaccess);
+
+ if (isset($user['summary']) && $user['summary']) {
+ $summary = $doc->createElement('cs:summary');
+ $summary->appendChild($doc->createTextNode($user['summary']));
+ $xuser->appendChild($summary);
+ }
+
+ $node->appendChild($xuser);
+
+ }
+
+
+ }
+
+ /**
+ * Unserializes the property.
+ *
+ * This static method should return a an instance of this object.
+ *
+ * @param \DOMElement $prop
+ * @return DAV\IProperty
+ */
+ static function unserialize(\DOMElement $prop) {
+
+ $xpath = new \DOMXPath($prop->ownerDocument);
+ $xpath->registerNamespace('cs', CalDAV\Plugin::NS_CALENDARSERVER);
+ $xpath->registerNamespace('d', 'urn:DAV');
+
+ $users = array();
+
+ foreach($xpath->query('cs:user', $prop) as $user) {
+
+ $status = null;
+ if ($xpath->evaluate('boolean(cs:invite-accepted)', $user)) {
+ $status = SharingPlugin::STATUS_ACCEPTED;
+ } elseif ($xpath->evaluate('boolean(cs:invite-declined)', $user)) {
+ $status = SharingPlugin::STATUS_DECLINED;
+ } elseif ($xpath->evaluate('boolean(cs:invite-noresponse)', $user)) {
+ $status = SharingPlugin::STATUS_NORESPONSE;
+ } elseif ($xpath->evaluate('boolean(cs:invite-invalid)', $user)) {
+ $status = SharingPlugin::STATUS_INVALID;
+ } else {
+ throw new DAV\Exception('Every cs:user property must have one of cs:invite-accepted, cs:invite-declined, cs:invite-noresponse or cs:invite-invalid');
+ }
+ $users[] = array(
+ 'href' => $xpath->evaluate('string(d:href)', $user),
+ 'commonName' => $xpath->evaluate('string(cs:common-name)', $user),
+ 'readOnly' => $xpath->evaluate('boolean(cs:access/cs:read)', $user),
+ 'summary' => $xpath->evaluate('string(cs:summary)', $user),
+ 'status' => $status,
+ );
+
+ }
+
+ return new self($users);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php
new file mode 100644
index 0000000..bf8f78a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/ScheduleCalendarTransp.php
@@ -0,0 +1,102 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * schedule-calendar-transp property.
+ *
+ * This property is a representation of the schedule-calendar-transp property.
+ * This property is defined in RFC6638 (caldav scheduling).
+ *
+ * Its values are either 'transparent' or 'opaque'. If it's transparent, it
+ * means that this calendar will not be taken into consideration when a
+ * different user queries for free-busy information. If it's 'opaque', it will.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ScheduleCalendarTransp extends DAV\Property {
+
+ const TRANSPARENT = 'transparent';
+ const OPAQUE = 'opaque';
+
+ protected $value;
+
+ /**
+ * Creates the property
+ *
+ * @param string $value
+ */
+ public function __construct($value) {
+
+ if ($value !== self::TRANSPARENT && $value !== self::OPAQUE) {
+ throw new \InvalidArgumentException('The value must either be specified as "transparent" or "opaque"');
+ }
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the current value
+ *
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ switch($this->value) {
+ case self::TRANSPARENT :
+ $xval = $doc->createElement('cal:transparent');
+ break;
+ case self::OPAQUE :
+ $xval = $doc->createElement('cal:opaque');
+ break;
+ }
+
+ $node->appendChild($xval);
+
+ }
+
+ /**
+ * Unserializes the DOMElement back into a Property class.
+ *
+ * @param \DOMElement $node
+ * @return ScheduleCalendarTransp
+ */
+ static function unserialize(\DOMElement $node) {
+
+ $value = null;
+ foreach($node->childNodes as $childNode) {
+ switch(DAV\XMLUtil::toClarkNotation($childNode)) {
+ case '{' . CalDAV\Plugin::NS_CALDAV . '}opaque' :
+ $value = self::OPAQUE;
+ break;
+ case '{' . CalDAV\Plugin::NS_CALDAV . '}transparent' :
+ $value = self::TRANSPARENT;
+ break;
+ }
+ }
+ if (is_null($value))
+ return null;
+
+ return new self($value);
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php
new file mode 100644
index 0000000..cd4b469
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarComponentSet.php
@@ -0,0 +1,88 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+
+/**
+ * Supported component set property
+ *
+ * This property is a representation of the supported-calendar_component-set
+ * property in the CalDAV namespace. It simply requires an array of components,
+ * such as VEVENT, VTODO
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedCalendarComponentSet extends DAV\Property {
+
+ /**
+ * List of supported components, such as "VEVENT, VTODO"
+ *
+ * @var array
+ */
+ private $components;
+
+ /**
+ * Creates the property
+ *
+ * @param array $components
+ */
+ public function __construct(array $components) {
+
+ $this->components = $components;
+
+ }
+
+ /**
+ * Returns the list of supported components
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->components;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ foreach($this->components as $component) {
+
+ $xcomp = $doc->createElement('cal:comp');
+ $xcomp->setAttribute('name',$component);
+ $node->appendChild($xcomp);
+
+ }
+
+ }
+
+ /**
+ * Unserializes the DOMElement back into a Property class.
+ *
+ * @param \DOMElement $node
+ * @return Property_SupportedCalendarComponentSet
+ */
+ static function unserialize(\DOMElement $node) {
+
+ $components = array();
+ foreach($node->childNodes as $childNode) {
+ if (DAV\XMLUtil::toClarkNotation($childNode)==='{' . CalDAV\Plugin::NS_CALDAV . '}comp') {
+ $components[] = $childNode->getAttribute('name');
+ }
+ }
+ return new self($components);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php
new file mode 100644
index 0000000..fd358c6
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCalendarData.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+use OldSabre\DAV;
+use OldSabre\CalDAV\Plugin;
+
+/**
+ * Supported-calendar-data property
+ *
+ * This property is a representation of the supported-calendar-data property
+ * in the CalDAV namespace. SabreDAV only has support for text/calendar;2.0
+ * so the value is currently hardcoded.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedCalendarData extends DAV\Property {
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $prefix = isset($server->xmlNamespaces[Plugin::NS_CALDAV])?$server->xmlNamespaces[Plugin::NS_CALDAV]:'cal';
+
+ $caldata = $doc->createElement($prefix . ':calendar-data');
+ $caldata->setAttribute('content-type','text/calendar');
+ $caldata->setAttribute('version','2.0');
+
+ $node->appendChild($caldata);
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php
new file mode 100644
index 0000000..9a92c7c
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Property/SupportedCollationSet.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace OldSabre\CalDAV\Property;
+use OldSabre\DAV;
+
+/**
+ * supported-collation-set property
+ *
+ * This property is a representation of the supported-collation-set property
+ * in the CalDAV namespace.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedCollationSet extends DAV\Property {
+
+ /**
+ * Serializes the property in a DOM document
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $prefix = $node->lookupPrefix('urn:ietf:params:xml:ns:caldav');
+ if (!$prefix) $prefix = 'cal';
+
+ $node->appendChild(
+ $doc->createElement($prefix . ':supported-collation','i;ascii-casemap')
+ );
+ $node->appendChild(
+ $doc->createElement($prefix . ':supported-collation','i;octet')
+ );
+ $node->appendChild(
+ $doc->createElement($prefix . ':supported-collation','i;unicode-casemap')
+ );
+
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php
new file mode 100644
index 0000000..5898917
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IMip.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\CalDAV\Schedule;
+
+use OldSabre\VObject;
+use OldSabre\DAV;
+
+/**
+ * iMIP handler.
+ *
+ * This class is responsible for sending out iMIP messages. iMIP is the
+ * email-based transport for iTIP. iTIP deals with scheduling operations for
+ * iCalendar objects.
+ *
+ * If you want to customize the email that gets sent out, you can do so by
+ * extending this class and overriding the sendMessage method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class IMip {
+
+ /**
+ * Email address used in From: header.
+ *
+ * @var string
+ */
+ protected $senderEmail;
+
+ /**
+ * Creates the email handler.
+ *
+ * @param string $senderEmail. The 'senderEmail' is the email that shows up
+ * in the 'From:' address. This should
+ * generally be some kind of no-reply email
+ * address you own.
+ */
+ public function __construct($senderEmail) {
+
+ $this->senderEmail = $senderEmail;
+
+ }
+
+ /**
+ * Sends one or more iTip messages through email.
+ *
+ * @param string $originator Originator Email
+ * @param array $recipients Array of email addresses
+ * @param VObject\Component $vObject
+ * @param string $principal Principal Url of the originator
+ * @return void
+ */
+ public function sendMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
+
+ foreach($recipients as $recipient) {
+
+ $to = $recipient;
+ $replyTo = $originator;
+ $subject = 'SabreDAV iTIP message';
+
+ switch(strtoupper($vObject->METHOD)) {
+ case 'REPLY' :
+ $subject = 'Response for: ' . $vObject->VEVENT->SUMMARY;
+ break;
+ case 'REQUEST' :
+ $subject = 'Invitation for: ' .$vObject->VEVENT->SUMMARY;
+ break;
+ case 'CANCEL' :
+ $subject = 'Cancelled event: ' . $vObject->VEVENT->SUMMARY;
+ break;
+ }
+
+ $headers = array();
+ $headers[] = 'Reply-To: ' . $replyTo;
+ $headers[] = 'From: ' . $this->senderEmail;
+ $headers[] = 'Content-Type: text/calendar; method=' . (string)$vObject->method . '; charset=utf-8';
+ if (DAV\Server::$exposeVersion) {
+ $headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION . '-' . DAV\Version::STABILITY;
+ }
+
+ $vcalBody = $vObject->serialize();
+
+ $this->mail($to, $subject, $vcalBody, $headers);
+
+ }
+
+ }
+
+ // @codeCoverageIgnoreStart
+ // This is deemed untestable in a reasonable manner
+
+ /**
+ * This function is reponsible for sending the actual email.
+ *
+ * @param string $to Recipient email address
+ * @param string $subject Subject of the email
+ * @param string $body iCalendar body
+ * @param array $headers List of headers
+ * @return void
+ */
+ protected function mail($to, $subject, $body, array $headers) {
+
+
+ mail($to, $subject, $body, implode("\r\n", $headers));
+
+ }
+
+ // @codeCoverageIgnoreEnd
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php
new file mode 100644
index 0000000..5040ac9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/IOutbox.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace OldSabre\CalDAV\Schedule;
+
+/**
+ * Implement this interface to have a node be recognized as a CalDAV scheduling
+ * outbox.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IOutbox extends \OldSabre\DAV\ICollection, \OldSabre\DAVACL\IACL {
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php
new file mode 100644
index 0000000..dc67766
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Schedule/Outbox.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace OldSabre\CalDAV\Schedule;
+use OldSabre\DAV;
+use OldSabre\CalDAV;
+use OldSabre\DAVACL;
+
+/**
+ * The CalDAV scheduling outbox
+ *
+ * The outbox is mainly used as an endpoint in the tree for a client to do
+ * free-busy requests. This functionality is completely handled by the
+ * Scheduling plugin, so this object is actually mostly static.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Outbox extends DAV\Collection implements IOutbox {
+
+ /**
+ * The principal Uri
+ *
+ * @var string
+ */
+ protected $principalUri;
+
+ /**
+ * Constructor
+ *
+ * @param string $principalUri
+ */
+ public function __construct($principalUri) {
+
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return 'outbox';
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return \OldSabre\DAV\INode[]
+ */
+ public function getChildren() {
+
+ return array();
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ),
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+ $default['aggregates'][] = array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+ );
+ $default['aggregates'][] = array(
+ 'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+ );
+
+ return $default;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php
new file mode 100644
index 0000000..2e8f440
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/ShareableCalendar.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This object represents a CalDAV calendar that can be shared with other
+ * users.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ShareableCalendar extends Calendar implements IShareableCalendar {
+
+ /**
+ * Updates the list of shares.
+ *
+ * The first array is a list of people that are to be added to the
+ * calendar.
+ *
+ * Every element in the add array has the following properties:
+ * * href - A url. Usually a mailto: address
+ * * commonName - Usually a first and last name, or false
+ * * summary - A description of the share, can also be false
+ * * readOnly - A boolean value
+ *
+ * Every element in the remove array is just the address string.
+ *
+ * @param array $add
+ * @param array $remove
+ * @return void
+ */
+ public function updateShares(array $add, array $remove) {
+
+ $this->caldavBackend->updateShares($this->calendarInfo['id'], $add, $remove);
+
+ }
+
+ /**
+ * Returns the list of people whom this calendar is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ public function getShares() {
+
+ return $this->caldavBackend->getShares($this->calendarInfo['id']);
+
+ }
+
+ /**
+ * Marks this calendar as published.
+ *
+ * Publishing a calendar should automatically create a read-only, public,
+ * subscribable calendar.
+ *
+ * @param bool $value
+ * @return void
+ */
+ public function setPublishStatus($value) {
+
+ $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php
new file mode 100644
index 0000000..5586419
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharedCalendar.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAVACL;
+
+/**
+ * This object represents a CalDAV calendar that is shared by a different user.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SharedCalendar extends Calendar implements ISharedCalendar {
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param array $calendarInfo
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
+
+ $required = array(
+ '{http://calendarserver.org/ns/}shared-url',
+ '{http://sabredav.org/ns}owner-principal',
+ '{http://sabredav.org/ns}read-only',
+ );
+ foreach($required as $r) {
+ if (!isset($calendarInfo[$r])) {
+ throw new \InvalidArgumentException('The ' . $r . ' property must be specified for SharedCalendar(s)');
+ }
+ }
+
+ parent::__construct($caldavBackend, $calendarInfo);
+
+ }
+
+ /**
+ * This method should return the url of the owners' copy of the shared
+ * calendar.
+ *
+ * @return string
+ */
+ public function getSharedUrl() {
+
+ return $this->calendarInfo['{http://calendarserver.org/ns/}shared-url'];
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->calendarInfo['{http://sabredav.org/ns}owner-principal'];
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ // The top-level ACL only contains access information for the true
+ // owner of the calendar, so we need to add the information for the
+ // sharee.
+ $acl = parent::getACL();
+ $acl[] = array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ );
+ if (!$this->calendarInfo['{http://sabredav.org/ns}read-only']) {
+ $acl[] = array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->calendarInfo['principaluri'],
+ 'protected' => true,
+ );
+ }
+ return $acl;
+
+ }
+
+ /**
+ * Returns the list of people whom this calendar is shared with.
+ *
+ * Every element in this array should have the following properties:
+ * * href - Often a mailto: address
+ * * commonName - Optional, for example a first + last name
+ * * status - See the OldSabre\CalDAV\SharingPlugin::STATUS_ constants.
+ * * readOnly - boolean
+ * * summary - Optional, a description for the share
+ *
+ * @return array
+ */
+ public function getShares() {
+
+ return $this->caldavBackend->getShares($this->calendarInfo['id']);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php
new file mode 100644
index 0000000..92942c0
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/SharingPlugin.php
@@ -0,0 +1,526 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+
+/**
+ * This plugin implements support for caldav sharing.
+ *
+ * This spec is defined at:
+ * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
+ *
+ * See:
+ * OldSabre\CalDAV\Backend\SharingSupport for all the documentation.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SharingPlugin extends DAV\ServerPlugin {
+
+ /**
+ * These are the various status constants used by sharing-messages.
+ */
+ const STATUS_ACCEPTED = 1;
+ const STATUS_DECLINED = 2;
+ const STATUS_DELETED = 3;
+ const STATUS_NORESPONSE = 4;
+ const STATUS_INVALID = 5;
+
+ /**
+ * Reference to SabreDAV server object.
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * This method should return a list of server-features.
+ *
+ * This is for example 'versioning' and is added to the DAV: header
+ * in an OPTIONS response.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('calendarserver-sharing');
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'caldav-sharing';
+
+ }
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by OldSabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->resourceTypeMapping['OldSabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared';
+
+ array_push(
+ $this->server->protectedProperties,
+ '{' . Plugin::NS_CALENDARSERVER . '}invite',
+ '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes',
+ '{' . Plugin::NS_CALENDARSERVER . '}shared-url'
+ );
+
+ $this->server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
+ $this->server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties'));
+ $this->server->subscribeEvent('updateProperties', array($this, 'updateProperties'));
+ $this->server->subscribeEvent('unknownMethod', array($this,'unknownMethod'));
+
+ }
+
+ /**
+ * This event is triggered when properties are requested for a certain
+ * node.
+ *
+ * This allows us to inject any properties early.
+ *
+ * @param string $path
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
+
+ if ($node instanceof IShareableCalendar) {
+ if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
+ new Property\Invite(
+ $node->getShares()
+ );
+
+ }
+
+ }
+
+ if ($node instanceof ISharedCalendar) {
+
+ if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}shared-url', $requestedProperties))!==false) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}shared-url'] =
+ new DAV\Property\Href(
+ $node->getSharedUrl()
+ );
+
+ }
+ // The 'invite' property is slightly different for the 'shared'
+ // instance of the calendar, as it also contains the owner
+ // information.
+ if (($index = array_search('{' . Plugin::NS_CALENDARSERVER . '}invite', $requestedProperties))!==false) {
+
+ unset($requestedProperties[$index]);
+
+ // Fetching owner information
+ $props = $this->server->getPropertiesForPath($node->getOwner(), array(
+ '{http://sabredav.org/ns}email-address',
+ '{DAV:}displayname',
+ ), 1);
+
+ $ownerInfo = array(
+ 'href' => $node->getOwner(),
+ );
+
+ if (isset($props[0][200])) {
+
+ // We're mapping the internal webdav properties to the
+ // elements caldav-sharing expects.
+ if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) {
+ $ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address'];
+ }
+ if (isset($props[0][200]['{DAV:}displayname'])) {
+ $ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname'];
+ }
+
+ }
+
+ $returnedProperties[200]['{' . Plugin::NS_CALENDARSERVER . '}invite'] =
+ new Property\Invite(
+ $node->getShares(),
+ $ownerInfo
+ );
+
+ }
+
+
+ }
+
+ }
+
+ /**
+ * This method is triggered *after* all properties have been retrieved.
+ * This allows us to inject the correct resourcetype for calendars that
+ * have been shared.
+ *
+ * @param string $path
+ * @param array $properties
+ * @param DAV\INode $node
+ * @return void
+ */
+ public function afterGetProperties($path, &$properties, DAV\INode $node) {
+
+ if ($node instanceof IShareableCalendar) {
+ if (isset($properties[200]['{DAV:}resourcetype'])) {
+ if (count($node->getShares())>0) {
+ $properties[200]['{DAV:}resourcetype']->add(
+ '{' . Plugin::NS_CALENDARSERVER . '}shared-owner'
+ );
+ }
+ }
+ $propName = '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes';
+ if (array_key_exists($propName, $properties[404])) {
+ unset($properties[404][$propName]);
+ $properties[200][$propName] = new Property\AllowedSharingModes(true,false);
+ }
+
+ }
+
+ }
+
+ /**
+ * This method is trigged when a user attempts to update a node's
+ * properties.
+ *
+ * A previous draft of the sharing spec stated that it was possible to use
+ * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing
+ * the calendar.
+ *
+ * Even though this is no longer in the current spec, we keep this around
+ * because OS X 10.7 may still make use of this feature.
+ *
+ * @param array $mutations
+ * @param array $result
+ * @param DAV\INode $node
+ * @return void
+ */
+ public function updateProperties(array &$mutations, array &$result, DAV\INode $node) {
+
+ if (!$node instanceof IShareableCalendar)
+ return;
+
+ if (!isset($mutations['{DAV:}resourcetype'])) {
+ return;
+ }
+
+ // Only doing something if shared-owner is indeed not in the list.
+ if($mutations['{DAV:}resourcetype']->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return;
+
+ $shares = $node->getShares();
+ $remove = array();
+ foreach($shares as $share) {
+ $remove[] = $share['href'];
+ }
+ $node->updateShares(array(), $remove);
+
+ // We're marking this update as 200 OK
+ $result[200]['{DAV:}resourcetype'] = null;
+
+ // Removing it from the mutations list
+ unset($mutations['{DAV:}resourcetype']);
+
+ }
+
+ /**
+ * This event is triggered when the server didn't know how to handle a
+ * certain request.
+ *
+ * We intercept this to handle POST requests on calendars.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return null|bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ if ($method!=='POST') {
+ return;
+ }
+
+ // Only handling xml
+ $contentType = $this->server->httpRequest->getHeader('Content-Type');
+ if (strpos($contentType,'application/xml')===false && strpos($contentType,'text/xml')===false)
+ return;
+
+ // Making sure the node exists
+ try {
+ $node = $this->server->tree->getNodeForPath($uri);
+ } catch (DAV\Exception\NotFound $e) {
+ return;
+ }
+
+ $requestBody = $this->server->httpRequest->getBody(true);
+
+ // If this request handler could not deal with this POST request, it
+ // will return 'null' and other plugins get a chance to handle the
+ // request.
+ //
+ // However, we already requested the full body. This is a problem,
+ // because a body can only be read once. This is why we preemptively
+ // re-populated the request body with the existing data.
+ $this->server->httpRequest->setBody($requestBody);
+
+ $dom = DAV\XMLUtil::loadDOMDocument($requestBody);
+
+ $documentType = DAV\XMLUtil::toClarkNotation($dom->firstChild);
+
+ switch($documentType) {
+
+ // Dealing with the 'share' document, which modified invitees on a
+ // calendar.
+ case '{' . Plugin::NS_CALENDARSERVER . '}share' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof IShareableCalendar) {
+ return;
+ }
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($uri, '{DAV:}write');
+ }
+
+ $mutations = $this->parseShareRequest($dom);
+
+ $node->updateShares($mutations[0], $mutations[1]);
+
+ $this->server->httpResponse->sendStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+
+ // The invite-reply document is sent when the user replies to an
+ // invitation of a calendar share.
+ case '{'. Plugin::NS_CALENDARSERVER.'}invite-reply' :
+
+ // This only works on the calendar-home-root node.
+ if (!$node instanceof UserCalendars) {
+ return;
+ }
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($uri, '{DAV:}write');
+ }
+
+ $message = $this->parseInviteReplyRequest($dom);
+
+ $url = $node->shareReply(
+ $message['href'],
+ $message['status'],
+ $message['calendarUri'],
+ $message['inReplyTo'],
+ $message['summary']
+ );
+
+ $this->server->httpResponse->sendStatus(200);
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ if ($url) {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->formatOutput = true;
+
+ $root = $dom->createElement('cs:shared-as');
+ foreach($this->server->xmlNamespaces as $namespace => $prefix) {
+ $root->setAttribute('xmlns:' . $prefix, $namespace);
+ }
+
+ $dom->appendChild($root);
+ $href = new DAV\Property\Href($url);
+
+ $href->serialize($this->server, $root);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml');
+ $this->server->httpResponse->sendBody($dom->saveXML());
+
+ }
+
+ // Breaking the event chain
+ return false;
+
+ case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof IShareableCalendar) {
+ return;
+ }
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($uri, '{DAV:}write');
+ }
+
+ $node->setPublishStatus(true);
+
+ // iCloud sends back the 202, so we will too.
+ $this->server->httpResponse->sendStatus(202);
+
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+
+ case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' :
+
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof IShareableCalendar) {
+ return;
+ }
+
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
+
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ $acl->checkPrivileges($uri, '{DAV:}write');
+ }
+
+ $node->setPublishStatus(false);
+
+ $this->server->httpResponse->sendStatus(200);
+
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $this->server->httpResponse->setHeader('X-Sabre-Status', 'everything-went-well');
+
+ // Breaking the event chain
+ return false;
+
+ }
+
+
+
+ }
+
+ /**
+ * Parses the 'share' POST request.
+ *
+ * This method returns an array, containing two arrays.
+ * The first array is a list of new sharees. Every element is a struct
+ * containing a:
+ * * href element. (usually a mailto: address)
+ * * commonName element (often a first and lastname, but can also be
+ * false)
+ * * readOnly (true or false)
+ * * summary (A description of the share, can also be false)
+ *
+ * The second array is a list of sharees that are to be removed. This is
+ * just a simple array with 'hrefs'.
+ *
+ * @param \DOMDocument $dom
+ * @return array
+ */
+ protected function parseShareRequest(\DOMDocument $dom) {
+
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNamespace('cs', Plugin::NS_CALENDARSERVER);
+ $xpath->registerNamespace('d', 'urn:DAV');
+
+ $set = array();
+ $elems = $xpath->query('cs:set');
+
+ for($i=0; $i < $elems->length; $i++) {
+
+ $xset = $elems->item($i);
+ $set[] = array(
+ 'href' => $xpath->evaluate('string(d:href)', $xset),
+ 'commonName' => $xpath->evaluate('string(cs:common-name)', $xset),
+ 'summary' => $xpath->evaluate('string(cs:summary)', $xset),
+ 'readOnly' => $xpath->evaluate('boolean(cs:read)', $xset)!==false
+ );
+
+ }
+
+ $remove = array();
+ $elems = $xpath->query('cs:remove');
+
+ for($i=0; $i < $elems->length; $i++) {
+
+ $xremove = $elems->item($i);
+ $remove[] = $xpath->evaluate('string(d:href)', $xremove);
+
+ }
+
+ return array($set, $remove);
+
+ }
+
+ /**
+ * Parses the 'invite-reply' POST request.
+ *
+ * This method returns an array, containing the following properties:
+ * * href - The sharee who is replying
+ * * status - One of the self::STATUS_* constants
+ * * calendarUri - The url of the shared calendar
+ * * inReplyTo - The unique id of the share invitation.
+ * * summary - Optional description of the reply.
+ *
+ * @param \DOMDocument $dom
+ * @return array
+ */
+ protected function parseInviteReplyRequest(\DOMDocument $dom) {
+
+ $xpath = new \DOMXPath($dom);
+ $xpath->registerNamespace('cs', Plugin::NS_CALENDARSERVER);
+ $xpath->registerNamespace('d', 'urn:DAV');
+
+ $hostHref = $xpath->evaluate('string(cs:hosturl/d:href)');
+ if (!$hostHref) {
+ throw new DAV\Exception\BadRequest('The {' . Plugin::NS_CALENDARSERVER . '}hosturl/{DAV:}href element is required');
+ }
+
+ return array(
+ 'href' => $xpath->evaluate('string(d:href)'),
+ 'calendarUri' => $this->server->calculateUri($hostHref),
+ 'inReplyTo' => $xpath->evaluate('string(cs:in-reply-to)'),
+ 'summary' => $xpath->evaluate('string(cs:summary)'),
+ 'status' => $xpath->evaluate('boolean(cs:invite-accepted)')?self::STATUS_ACCEPTED:self::STATUS_DECLINED
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php
new file mode 100644
index 0000000..a4fbc66
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/UserCalendars.php
@@ -0,0 +1,342 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * The UserCalenders class contains all calendars associated to one user
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UserCalendars implements DAV\IExtendedCollection, DAVACL\IACL {
+
+ /**
+ * CalDAV backend
+ *
+ * @var OldSabre\CalDAV\Backend\BackendInterface
+ */
+ protected $caldavBackend;
+
+ /**
+ * Principal information
+ *
+ * @var array
+ */
+ protected $principalInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $caldavBackend
+ * @param mixed $userUri
+ */
+ public function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) {
+
+ $this->caldavBackend = $caldavBackend;
+ $this->principalInfo = $principalInfo;
+
+ }
+
+ /**
+ * Returns the name of this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(,$name) = DAV\URLUtil::splitPath($this->principalInfo['uri']);
+ return $name;
+
+ }
+
+ /**
+ * Updates the name of this object
+ *
+ * @param string $name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\Forbidden();
+
+ }
+
+ /**
+ * Deletes this object
+ *
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\Forbidden();
+
+ }
+
+ /**
+ * Returns the last modification date
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $filename
+ * @param resource $data
+ * @return void
+ */
+ public function createFile($filename, $data=null) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * This is currently not allowed.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function createDirectory($filename) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+
+ }
+
+ /**
+ * Returns a single calendar, by name
+ *
+ * @param string $name
+ * @todo needs optimizing
+ * @return Calendar
+ */
+ public function getChild($name) {
+
+ foreach($this->getChildren() as $child) {
+ if ($name==$child->getName())
+ return $child;
+
+ }
+ throw new DAV\Exception\NotFound('Calendar with name \'' . $name . '\' could not be found');
+
+ }
+
+ /**
+ * Checks if a calendar exists.
+ *
+ * @param string $name
+ * @todo needs optimizing
+ * @return bool
+ */
+ public function childExists($name) {
+
+ foreach($this->getChildren() as $child) {
+ if ($name==$child->getName())
+ return true;
+
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns a list of calendars
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
+ $objs = array();
+ foreach($calendars as $calendar) {
+ if ($this->caldavBackend instanceof Backend\SharingSupport) {
+ if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
+ $objs[] = new SharedCalendar($this->caldavBackend, $calendar);
+ } else {
+ $objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
+ }
+ } else {
+ $objs[] = new Calendar($this->caldavBackend, $calendar);
+ }
+ }
+ $objs[] = new Schedule\Outbox($this->principalInfo['uri']);
+
+ // We're adding a notifications node, if it's supported by the backend.
+ if ($this->caldavBackend instanceof Backend\NotificationSupport) {
+ $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+ }
+ return $objs;
+
+ }
+
+ /**
+ * Creates a new calendar
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ public function createExtendedCollection($name, array $resourceType, array $properties) {
+
+ $isCalendar = false;
+ foreach($resourceType as $rt) {
+ switch ($rt) {
+ case '{DAV:}collection' :
+ case '{http://calendarserver.org/ns/}shared-owner' :
+ // ignore
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}calendar' :
+ $isCalendar = true;
+ break;
+ default :
+ throw new DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt);
+ }
+ }
+ if (!$isCalendar) {
+ throw new DAV\Exception\InvalidResourceType('You can only create calendars in this collection');
+ }
+ $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalInfo['uri'];
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+ /**
+ * This method is called when a user replied to a request to share.
+ *
+ * This method should return the url of the newly created calendar if the
+ * share was accepted.
+ *
+ * @param string href The sharee who is replying (often a mailto: address)
+ * @param int status One of the SharingPlugin::STATUS_* constants
+ * @param string $calendarUri The url to the calendar thats being shared
+ * @param string $inReplyTo The unique id this message is a response to
+ * @param string $summary A description of the reply
+ * @return null|string
+ */
+ public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) {
+
+ if (!$this->caldavBackend instanceof Backend\SharingSupport) {
+ throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
+ }
+
+ return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php
new file mode 100644
index 0000000..ead559a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CalDAV/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\CalDAV;
+
+/**
+ * This class contains the OldSabre\CalDAV version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php
new file mode 100644
index 0000000..250c0db
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBook.php
@@ -0,0 +1,315 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * The AddressBook class represents a CardDAV addressbook, owned by a specific user
+ *
+ * The AddressBook can contain multiple vcards
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AddressBook extends DAV\Collection implements IAddressBook, DAV\IProperties, DAVACL\IACL {
+
+ /**
+ * This is an array with addressbook information
+ *
+ * @var array
+ */
+ protected $addressBookInfo;
+
+ /**
+ * CardDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $carddavBackend
+ * @param array $addressBookInfo
+ */
+ public function __construct(Backend\BackendInterface $carddavBackend, array $addressBookInfo) {
+
+ $this->carddavBackend = $carddavBackend;
+ $this->addressBookInfo = $addressBookInfo;
+
+ }
+
+ /**
+ * Returns the name of the addressbook
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->addressBookInfo['uri'];
+
+ }
+
+ /**
+ * Returns a card
+ *
+ * @param string $name
+ * @return \ICard
+ */
+ public function getChild($name) {
+
+ $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'],$name);
+ if (!$obj) throw new DAV\Exception\NotFound('Card not found');
+ return new Card($this->carddavBackend,$this->addressBookInfo,$obj);
+
+ }
+
+ /**
+ * Returns the full list of cards
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']);
+ $children = array();
+ foreach($objs as $obj) {
+ $children[] = new Card($this->carddavBackend,$this->addressBookInfo,$obj);
+ }
+ return $children;
+
+ }
+
+ /**
+ * Creates a new directory
+ *
+ * We actually block this, as subdirectories are not allowed in addressbooks.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed');
+
+ }
+
+ /**
+ * Creates a new file
+ *
+ * The contents of the new file must be a valid VCARD.
+ *
+ * This method may return an ETag.
+ *
+ * @param string $name
+ * @param resource $vcardData
+ * @return string|null
+ */
+ public function createFile($name,$vcardData = null) {
+
+ if (is_resource($vcardData)) {
+ $vcardData = stream_get_contents($vcardData);
+ }
+ // Converting to UTF-8, if needed
+ $vcardData = DAV\StringUtil::ensureUTF8($vcardData);
+
+ return $this->carddavBackend->createCard($this->addressBookInfo['id'],$name,$vcardData);
+
+ }
+
+ /**
+ * Deletes the entire addressbook.
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']);
+
+ }
+
+ /**
+ * Renames the addressbook
+ *
+ * @param string $newName
+ * @return void
+ */
+ public function setName($newName) {
+
+ throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported');
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp.
+ *
+ * @return void
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Updates properties on this node,
+ *
+ * The properties array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => array(
+ * '{DAV:}displayname' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}owner' => null,
+ * )
+ * )
+ *
+ * In this example it was forbidden to update {DAV:}displayname.
+ * (403 Forbidden), which in turn also caused {DAV:}owner to fail
+ * (424 Failed Dependency) because the request needs to be atomic.
+ *
+ * @param array $mutations
+ * @return bool|array
+ */
+ public function updateProperties($mutations) {
+
+ return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $mutations);
+
+ }
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * @param array $properties
+ * @return array
+ */
+ public function getProperties($properties) {
+
+ $response = array();
+ foreach($properties as $propertyName) {
+
+ if (isset($this->addressBookInfo[$propertyName])) {
+
+ $response[$propertyName] = $this->addressBookInfo[$propertyName];
+
+ }
+
+ }
+
+ return $response;
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->addressBookInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php
new file mode 100644
index 0000000..e754147
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookQueryParser.php
@@ -0,0 +1,221 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+
+/**
+ * Parses the addressbook-query report request body.
+ *
+ * Whoever designed this format, and the CalDAV equivalent even more so,
+ * has no feel for design.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AddressBookQueryParser {
+
+ const TEST_ANYOF = 'anyof';
+ const TEST_ALLOF = 'allof';
+
+ /**
+ * List of requested properties the client wanted
+ *
+ * @var array
+ */
+ public $requestedProperties;
+
+ /**
+ * The number of results the client wants
+ *
+ * null means it wasn't specified, which in most cases means 'all results'.
+ *
+ * @var int|null
+ */
+ public $limit;
+
+ /**
+ * List of property filters.
+ *
+ * @var array
+ */
+ public $filters;
+
+ /**
+ * Either TEST_ANYOF or TEST_ALLOF
+ *
+ * @var string
+ */
+ public $test;
+
+ /**
+ * DOM Document
+ *
+ * @var DOMDocument
+ */
+ protected $dom;
+
+ /**
+ * DOM XPath object
+ *
+ * @var DOMXPath
+ */
+ protected $xpath;
+
+ /**
+ * Creates the parser
+ *
+ * @param \DOMDocument $dom
+ */
+ public function __construct(\DOMDocument $dom) {
+
+ $this->dom = $dom;
+
+ $this->xpath = new \DOMXPath($dom);
+ $this->xpath->registerNameSpace('card',Plugin::NS_CARDDAV);
+
+ }
+
+ /**
+ * Parses the request.
+ *
+ * @return void
+ */
+ public function parse() {
+
+ $filterNode = null;
+
+ $limit = $this->xpath->evaluate('number(/card:addressbook-query/card:limit/card:nresults)');
+ if (is_nan($limit)) $limit = null;
+
+ $filter = $this->xpath->query('/card:addressbook-query/card:filter');
+
+ // According to the CardDAV spec there needs to be exactly 1 filter
+ // element. However, KDE 4.8.2 contains a bug that will encode 0 filter
+ // elements, so this is a workaround for that.
+ //
+ // See: https://bugs.kde.org/show_bug.cgi?id=300047
+ if ($filter->length === 0) {
+ $test = null;
+ $filter = null;
+ } elseif ($filter->length === 1) {
+ $filter = $filter->item(0);
+ $test = $this->xpath->evaluate('string(@test)', $filter);
+ } else {
+ throw new DAV\Exception\BadRequest('Only one filter element is allowed');
+ }
+
+ if (!$test) $test = self::TEST_ANYOF;
+ if ($test !== self::TEST_ANYOF && $test !== self::TEST_ALLOF) {
+ throw new DAV\Exception\BadRequest('The test attribute must either hold "anyof" or "allof"');
+ }
+
+ $propFilters = array();
+
+ $propFilterNodes = $this->xpath->query('card:prop-filter', $filter);
+ for($ii=0; $ii < $propFilterNodes->length; $ii++) {
+
+ $propFilters[] = $this->parsePropFilterNode($propFilterNodes->item($ii));
+
+
+ }
+
+ $this->filters = $propFilters;
+ $this->limit = $limit;
+ $this->requestedProperties = array_keys(DAV\XMLUtil::parseProperties($this->dom->firstChild));
+ $this->test = $test;
+
+ }
+
+ /**
+ * Parses the prop-filter xml element
+ *
+ * @param \DOMElement $propFilterNode
+ * @return array
+ */
+ protected function parsePropFilterNode(\DOMElement $propFilterNode) {
+
+ $propFilter = array();
+ $propFilter['name'] = $propFilterNode->getAttribute('name');
+ $propFilter['test'] = $propFilterNode->getAttribute('test');
+ if (!$propFilter['test']) $propFilter['test'] = 'anyof';
+
+ $propFilter['is-not-defined'] = $this->xpath->query('card:is-not-defined', $propFilterNode)->length>0;
+
+ $paramFilterNodes = $this->xpath->query('card:param-filter', $propFilterNode);
+
+ $propFilter['param-filters'] = array();
+
+
+ for($ii=0;$ii<$paramFilterNodes->length;$ii++) {
+
+ $propFilter['param-filters'][] = $this->parseParamFilterNode($paramFilterNodes->item($ii));
+
+ }
+ $propFilter['text-matches'] = array();
+ $textMatchNodes = $this->xpath->query('card:text-match', $propFilterNode);
+
+ for($ii=0;$ii<$textMatchNodes->length;$ii++) {
+
+ $propFilter['text-matches'][] = $this->parseTextMatchNode($textMatchNodes->item($ii));
+
+ }
+
+ return $propFilter;
+
+ }
+
+ /**
+ * Parses the param-filter element
+ *
+ * @param \DOMElement $paramFilterNode
+ * @return array
+ */
+ public function parseParamFilterNode(\DOMElement $paramFilterNode) {
+
+ $paramFilter = array();
+ $paramFilter['name'] = $paramFilterNode->getAttribute('name');
+ $paramFilter['is-not-defined'] = $this->xpath->query('card:is-not-defined', $paramFilterNode)->length>0;
+ $paramFilter['text-match'] = null;
+
+ $textMatch = $this->xpath->query('card:text-match', $paramFilterNode);
+ if ($textMatch->length>0) {
+ $paramFilter['text-match'] = $this->parseTextMatchNode($textMatch->item(0));
+ }
+
+ return $paramFilter;
+
+ }
+
+ /**
+ * Text match
+ *
+ * @param \DOMElement $textMatchNode
+ * @return array
+ */
+ public function parseTextMatchNode(\DOMElement $textMatchNode) {
+
+ $matchType = $textMatchNode->getAttribute('match-type');
+ if (!$matchType) $matchType = 'contains';
+
+ if (!in_array($matchType, array('contains', 'equals', 'starts-with', 'ends-with'))) {
+ throw new DAV\Exception\BadRequest('Unknown match-type: ' . $matchType);
+ }
+
+ $negateCondition = $textMatchNode->getAttribute('negate-condition');
+ $negateCondition = $negateCondition==='yes';
+ $collation = $textMatchNode->getAttribute('collation');
+ if (!$collation) $collation = 'i;unicode-casemap';
+
+ return array(
+ 'negate-condition' => $negateCondition,
+ 'collation' => $collation,
+ 'match-type' => $matchType,
+ 'value' => $textMatchNode->nodeValue
+ );
+
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php
new file mode 100644
index 0000000..65c2271
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/AddressBookRoot.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAVACL;
+
+/**
+ * AddressBook rootnode
+ *
+ * This object lists a collection of users, which can contain addressbooks.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AddressBookRoot extends DAVACL\AbstractPrincipalCollection {
+
+ /**
+ * Principal Backend
+ *
+ * @var OldSabre\DAVACL\PrincipalBackend\BackendInteface
+ */
+ protected $principalBackend;
+
+ /**
+ * CardDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Constructor
+ *
+ * This constructor needs both a principal and a carddav backend.
+ *
+ * By default this class will show a list of addressbook collections for
+ * principals in the 'principals' collection. If your main principals are
+ * actually located in a different path, use the $principalPrefix argument
+ * to override this.
+ *
+ * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+ * @param Backend\BackendInterface $carddavBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend,Backend\BackendInterface $carddavBackend, $principalPrefix = 'principals') {
+
+ $this->carddavBackend = $carddavBackend;
+ parent::__construct($principalBackend, $principalPrefix);
+
+ }
+
+ /**
+ * Returns the name of the node
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return Plugin::ADDRESSBOOK_ROOT;
+
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \OldSabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal) {
+
+ return new UserAddressBooks($this->carddavBackend, $principal['uri']);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php
new file mode 100644
index 0000000..6b90e0e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/AbstractBackend.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace OldSabre\CardDAV\Backend;
+
+/**
+ * CardDAV abstract Backend
+ *
+ * This class serves as a base-class for addressbook backends
+ *
+ * This class doesn't do much, but it was added for consistency.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php
new file mode 100644
index 0000000..9f9e1fa
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/BackendInterface.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace OldSabre\CardDAV\Backend;
+
+/**
+ * CardDAV Backend Interface
+ *
+ * Any CardDAV backend must implement this interface.
+ *
+ * Note that there are references to 'addressBookId' scattered throughout the
+ * class. The value of the addressBookId is completely up to you, it can be any
+ * arbitrary value you can use as an unique identifier.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * Every addressbook should have the following properties:
+ * id - an arbitrary unique id
+ * uri - the 'basename' part of the url
+ * principaluri - Same as the passed parameter
+ *
+ * Any additional clark-notation property may be passed besides this. Some
+ * common ones are :
+ * {DAV:}displayname
+ * {urn:ietf:params:xml:ns:carddav}addressbook-description
+ * {http://calendarserver.org/ns/}getctag
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri);
+
+ /**
+ * Updates an addressbook's properties
+ *
+ * See OldSabre\DAV\IProperties for a description of the mutations array, as
+ * well as the return value.
+ *
+ * @param mixed $addressBookId
+ * @param array $mutations
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateAddressBook($addressBookId, array $mutations);
+
+ /**
+ * Creates a new address book
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ * @return void
+ */
+ public function createAddressBook($principalUri, $url, array $properties);
+
+ /**
+ * Deletes an entire addressbook and all its contents
+ *
+ * @param mixed $addressBookId
+ * @return void
+ */
+ public function deleteAddressBook($addressBookId);
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ * @return array
+ */
+ public function getCards($addressbookId);
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri);
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData);
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData);
+
+ /**
+ * Deletes a card
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php
new file mode 100644
index 0000000..e20b980
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Backend/PDO.php
@@ -0,0 +1,333 @@
+<?php
+
+namespace OldSabre\CardDAV\Backend;
+
+use OldSabre\CardDAV;
+use OldSabre\DAV;
+
+/**
+ * PDO CardDAV backend
+ *
+ * This CardDAV backend uses PDO to store addressbooks
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * PDO connection
+ *
+ * @var PDO
+ */
+ protected $pdo;
+
+ /**
+ * The PDO table name used to store addressbooks
+ */
+ protected $addressBooksTableName;
+
+ /**
+ * The PDO table name used to store cards
+ */
+ protected $cardsTableName;
+
+ /**
+ * Sets up the object
+ *
+ * @param \PDO $pdo
+ * @param string $addressBooksTableName
+ * @param string $cardsTableName
+ */
+ public function __construct(\PDO $pdo, $addressBooksTableName = 'addressbooks', $cardsTableName = 'cards') {
+
+ $this->pdo = $pdo;
+ $this->addressBooksTableName = $addressBooksTableName;
+ $this->cardsTableName = $cardsTableName;
+
+ }
+
+ /**
+ * Returns the list of addressbooks for a specific user.
+ *
+ * @param string $principalUri
+ * @return array
+ */
+ public function getAddressBooksForUser($principalUri) {
+
+ $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, ctag FROM '.$this->addressBooksTableName.' WHERE principaluri = ?');
+ $stmt->execute(array($principalUri));
+
+ $addressBooks = array();
+
+ foreach($stmt->fetchAll() as $row) {
+
+ $addressBooks[] = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ '{DAV:}displayname' => $row['displayname'],
+ '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' => $row['description'],
+ '{http://calendarserver.org/ns/}getctag' => $row['ctag'],
+ '{' . CardDAV\Plugin::NS_CARDDAV . '}supported-address-data' =>
+ new CardDAV\Property\SupportedAddressData(),
+ );
+
+ }
+
+ return $addressBooks;
+
+ }
+
+
+ /**
+ * Updates an addressbook's properties
+ *
+ * See OldSabre\DAV\IProperties for a description of the mutations array, as
+ * well as the return value.
+ *
+ * @param mixed $addressBookId
+ * @param array $mutations
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateAddressBook($addressBookId, array $mutations) {
+
+ $updates = array();
+
+ foreach($mutations as $property=>$newValue) {
+
+ switch($property) {
+ case '{DAV:}displayname' :
+ $updates['displayname'] = $newValue;
+ break;
+ case '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' :
+ $updates['description'] = $newValue;
+ break;
+ default :
+ // If any unsupported values were being updated, we must
+ // let the entire request fail.
+ return false;
+ }
+
+ }
+
+ // No values are being updated?
+ if (!$updates) {
+ return false;
+ }
+
+ $query = 'UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 ';
+ foreach($updates as $key=>$value) {
+ $query.=', `' . $key . '` = :' . $key . ' ';
+ }
+ $query.=' WHERE id = :addressbookid';
+
+ $stmt = $this->pdo->prepare($query);
+ $updates['addressbookid'] = $addressBookId;
+
+ $stmt->execute($updates);
+
+ return true;
+
+ }
+
+ /**
+ * Creates a new address book
+ *
+ * @param string $principalUri
+ * @param string $url Just the 'basename' of the url.
+ * @param array $properties
+ * @return void
+ */
+ public function createAddressBook($principalUri, $url, array $properties) {
+
+ $values = array(
+ 'displayname' => null,
+ 'description' => null,
+ 'principaluri' => $principalUri,
+ 'uri' => $url,
+ );
+
+ foreach($properties as $property=>$newValue) {
+
+ switch($property) {
+ case '{DAV:}displayname' :
+ $values['displayname'] = $newValue;
+ break;
+ case '{' . CardDAV\Plugin::NS_CARDDAV . '}addressbook-description' :
+ $values['description'] = $newValue;
+ break;
+ default :
+ throw new DAV\Exception\BadRequest('Unknown property: ' . $property);
+ }
+
+ }
+
+ $query = 'INSERT INTO ' . $this->addressBooksTableName . ' (uri, displayname, description, principaluri, ctag) VALUES (:uri, :displayname, :description, :principaluri, 1)';
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ }
+
+ /**
+ * Deletes an entire addressbook and all its contents
+ *
+ * @param int $addressBookId
+ * @return void
+ */
+ public function deleteAddressBook($addressBookId) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ?');
+ $stmt->execute(array($addressBookId));
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->addressBooksTableName . ' WHERE id = ?');
+ $stmt->execute(array($addressBookId));
+
+ }
+
+ /**
+ * Returns all cards for a specific addressbook id.
+ *
+ * This method should return the following properties for each card:
+ * * carddata - raw vcard data
+ * * uri - Some unique url
+ * * lastmodified - A unix timestamp
+ *
+ * It's recommended to also return the following properties:
+ * * etag - A unique etag. This must change every time the card changes.
+ * * size - The size of the card in bytes.
+ *
+ * If these last two properties are provided, less time will be spent
+ * calculating them. If they are specified, you can also ommit carddata.
+ * This may speed up certain requests, especially with large cards.
+ *
+ * @param mixed $addressbookId
+ * @return array
+ */
+ public function getCards($addressbookId) {
+
+ $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ?');
+ $stmt->execute(array($addressbookId));
+
+ return $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+
+ }
+
+ /**
+ * Returns a specfic card.
+ *
+ * The same set of properties must be returned as with getCards. The only
+ * exception is that 'carddata' is absolutely required.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return array
+ */
+ public function getCard($addressBookId, $cardUri) {
+
+ $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ? LIMIT 1');
+ $stmt->execute(array($addressBookId, $cardUri));
+
+ $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
+
+ return (count($result)>0?$result[0]:false);
+
+ }
+
+ /**
+ * Creates a new card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag is for the
+ * newly created resource, and must be enclosed with double quotes (that
+ * is, the string itself must contain the double quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function createCard($addressBookId, $cardUri, $cardData) {
+
+ $stmt = $this->pdo->prepare('INSERT INTO ' . $this->cardsTableName . ' (carddata, uri, lastmodified, addressbookid) VALUES (?, ?, ?, ?)');
+
+ $result = $stmt->execute(array($cardData, $cardUri, time(), $addressBookId));
+
+ $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt2->execute(array($addressBookId));
+
+ return '"' . md5($cardData) . '"';
+
+ }
+
+ /**
+ * Updates a card.
+ *
+ * The addressbook id will be passed as the first argument. This is the
+ * same id as it is returned from the getAddressbooksForUser method.
+ *
+ * The cardUri is a base uri, and doesn't include the full path. The
+ * cardData argument is the vcard body, and is passed as a string.
+ *
+ * It is possible to return an ETag from this method. This ETag should
+ * match that of the updated resource, and must be enclosed with double
+ * quotes (that is: the string itself must contain the actual quotes).
+ *
+ * You should only return the ETag if you store the carddata as-is. If a
+ * subsequent GET request on the same card does not have the same body,
+ * byte-by-byte and you did return an ETag here, clients tend to get
+ * confused.
+ *
+ * If you don't return an ETag, you can just return null.
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @param string $cardData
+ * @return string|null
+ */
+ public function updateCard($addressBookId, $cardUri, $cardData) {
+
+ $stmt = $this->pdo->prepare('UPDATE ' . $this->cardsTableName . ' SET carddata = ?, lastmodified = ? WHERE uri = ? AND addressbookid =?');
+ $stmt->execute(array($cardData, time(), $cardUri, $addressBookId));
+
+ $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt2->execute(array($addressBookId));
+
+ return '"' . md5($cardData) . '"';
+
+ }
+
+ /**
+ * Deletes a card
+ *
+ * @param mixed $addressBookId
+ * @param string $cardUri
+ * @return bool
+ */
+ public function deleteCard($addressBookId, $cardUri) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM ' . $this->cardsTableName . ' WHERE addressbookid = ? AND uri = ?');
+ $stmt->execute(array($addressBookId, $cardUri));
+
+ $stmt2 = $this->pdo->prepare('UPDATE ' . $this->addressBooksTableName . ' SET ctag = ctag + 1 WHERE id = ?');
+ $stmt2->execute(array($addressBookId));
+
+ return $stmt->rowCount()===1;
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php
new file mode 100644
index 0000000..08ab828
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Card.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAVACL;
+use OldSabre\DAV;
+
+
+/**
+ * The Card object represents a single Card from an addressbook
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Card extends DAV\File implements ICard, DAVACL\IACL {
+
+ /**
+ * CardDAV backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Array with information about this Card
+ *
+ * @var array
+ */
+ protected $cardData;
+
+ /**
+ * Array with information about the containing addressbook
+ *
+ * @var array
+ */
+ protected $addressBookInfo;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $carddavBackend
+ * @param array $addressBookInfo
+ * @param array $cardData
+ */
+ public function __construct(Backend\BackendInterface $carddavBackend,array $addressBookInfo,array $cardData) {
+
+ $this->carddavBackend = $carddavBackend;
+ $this->addressBookInfo = $addressBookInfo;
+ $this->cardData = $cardData;
+
+ }
+
+ /**
+ * Returns the uri for this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->cardData['uri'];
+
+ }
+
+ /**
+ * Returns the VCard-formatted object
+ *
+ * @return string
+ */
+ public function get() {
+
+ // Pre-populating 'carddata' is optional. If we don't yet have it
+ // already, we fetch it from the backend.
+ if (!isset($this->cardData['carddata'])) {
+ $this->cardData = $this->carddavBackend->getCard($this->addressBookInfo['id'], $this->cardData['uri']);
+ }
+ return $this->cardData['carddata'];
+
+ }
+
+ /**
+ * Updates the VCard-formatted object
+ *
+ * @param string $cardData
+ * @return string|null
+ */
+ public function put($cardData) {
+
+ if (is_resource($cardData))
+ $cardData = stream_get_contents($cardData);
+
+ // Converting to UTF-8, if needed
+ $cardData = DAV\StringUtil::ensureUTF8($cardData);
+
+ $etag = $this->carddavBackend->updateCard($this->addressBookInfo['id'],$this->cardData['uri'],$cardData);
+ $this->cardData['carddata'] = $cardData;
+ $this->cardData['etag'] = $etag;
+
+ return $etag;
+
+ }
+
+ /**
+ * Deletes the card
+ *
+ * @return void
+ */
+ public function delete() {
+
+ $this->carddavBackend->deleteCard($this->addressBookInfo['id'],$this->cardData['uri']);
+
+ }
+
+ /**
+ * Returns the mime content-type
+ *
+ * @return string
+ */
+ public function getContentType() {
+
+ return 'text/x-vcard; charset=utf-8';
+
+ }
+
+ /**
+ * Returns an ETag for this object
+ *
+ * @return string
+ */
+ public function getETag() {
+
+ if (isset($this->cardData['etag'])) {
+ return $this->cardData['etag'];
+ } else {
+ $data = $this->get();
+ if (is_string($data)) {
+ return '"' . md5($data) . '"';
+ } else {
+ // We refuse to calculate the md5 if it's a stream.
+ return null;
+ }
+ }
+
+ }
+
+ /**
+ * Returns the last modification date as a unix timestamp
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return isset($this->cardData['lastmodified'])?$this->cardData['lastmodified']:null;
+
+ }
+
+ /**
+ * Returns the size of this object in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ if (array_key_exists('size', $this->cardData)) {
+ return $this->cardData['size'];
+ } else {
+ return strlen($this->get());
+ }
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->addressBookInfo['principaluri'];
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->addressBookInfo['principaluri'],
+ 'protected' => true,
+ ),
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php
new file mode 100644
index 0000000..9670c85
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IAddressBook.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+
+/**
+ * AddressBook interface
+ *
+ * Implement this interface to allow a node to be recognized as an addressbook.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IAddressBook extends DAV\ICollection {
+
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php
new file mode 100644
index 0000000..9d8945f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/ICard.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+
+/**
+ * Card interface
+ *
+ * Extend the ICard interface to allow your custom nodes to be picked up as
+ * 'Cards'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICard extends DAV\IFile {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php
new file mode 100644
index 0000000..7d3a3bd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/IDirectory.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+/**
+ * IDirectory interface
+ *
+ * Implement this interface to have an addressbook marked as a 'directory'. A
+ * directory is an (often) global addressbook.
+ *
+ * A full description can be found in the IETF draft:
+ * - draft-daboo-carddav-directory-gateway
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IDirectory extends IAddressBook {
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php
new file mode 100644
index 0000000..fa3077b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Plugin.php
@@ -0,0 +1,706 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+use OldSabre\VObject;
+
+/**
+ * CardDAV plugin
+ *
+ * The CardDAV plugin adds CardDAV functionality to the WebDAV server
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Url to the addressbooks
+ */
+ const ADDRESSBOOK_ROOT = 'addressbooks';
+
+ /**
+ * xml namespace for CardDAV elements
+ */
+ const NS_CARDDAV = 'urn:ietf:params:xml:ns:carddav';
+
+ /**
+ * Add urls to this property to have them automatically exposed as
+ * 'directories' to the user.
+ *
+ * @var array
+ */
+ public $directories = array();
+
+ /**
+ * Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ /* Events */
+ $server->subscribeEvent('beforeGetProperties', array($this, 'beforeGetProperties'));
+ $server->subscribeEvent('afterGetProperties', array($this, 'afterGetProperties'));
+ $server->subscribeEvent('updateProperties', array($this, 'updateProperties'));
+ $server->subscribeEvent('report', array($this,'report'));
+ $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
+ $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
+ $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
+ $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
+
+ /* Namespaces */
+ $server->xmlNamespaces[self::NS_CARDDAV] = 'card';
+
+ /* Mapping Interfaces to {DAV:}resourcetype values */
+ $server->resourceTypeMapping['OldSabre\\CardDAV\\IAddressBook'] = '{' . self::NS_CARDDAV . '}addressbook';
+ $server->resourceTypeMapping['OldSabre\\CardDAV\\IDirectory'] = '{' . self::NS_CARDDAV . '}directory';
+
+ /* Adding properties that may never be changed */
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-address-data';
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}max-resource-size';
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}addressbook-home-set';
+ $server->protectedProperties[] = '{' . self::NS_CARDDAV . '}supported-collation-set';
+
+ $server->propertyMap['{http://calendarserver.org/ns/}me-card'] = 'OldSabre\\DAV\\Property\\Href';
+
+ $this->server = $server;
+
+ }
+
+ /**
+ * Returns a list of supported features.
+ *
+ * This is used in the DAV: header in the OPTIONS and PROPFIND requests.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('addressbook');
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ $node = $this->server->tree->getNodeForPath($uri);
+ if ($node instanceof IAddressBook || $node instanceof ICard) {
+ return array(
+ '{' . self::NS_CARDDAV . '}addressbook-multiget',
+ '{' . self::NS_CARDDAV . '}addressbook-query',
+ );
+ }
+ return array();
+
+ }
+
+
+ /**
+ * Adds all CardDAV-specific properties
+ *
+ * @param string $path
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @return void
+ */
+ public function beforeGetProperties($path, DAV\INode $node, array &$requestedProperties, array &$returnedProperties) {
+
+ if ($node instanceof DAVACL\IPrincipal) {
+
+ // calendar-home-set property
+ $addHome = '{' . self::NS_CARDDAV . '}addressbook-home-set';
+ if (in_array($addHome,$requestedProperties)) {
+ $principalId = $node->getName();
+ $addressbookHomePath = self::ADDRESSBOOK_ROOT . '/' . $principalId . '/';
+ unset($requestedProperties[array_search($addHome, $requestedProperties)]);
+ $returnedProperties[200][$addHome] = new DAV\Property\Href($addressbookHomePath);
+ }
+
+ $directories = '{' . self::NS_CARDDAV . '}directory-gateway';
+ if ($this->directories && in_array($directories, $requestedProperties)) {
+ unset($requestedProperties[array_search($directories, $requestedProperties)]);
+ $returnedProperties[200][$directories] = new DAV\Property\HrefList($this->directories);
+ }
+
+ }
+
+ if ($node instanceof ICard) {
+
+ // The address-data property is not supposed to be a 'real'
+ // property, but in large chunks of the spec it does act as such.
+ // Therefore we simply expose it as a property.
+ $addressDataProp = '{' . self::NS_CARDDAV . '}address-data';
+ if (in_array($addressDataProp, $requestedProperties)) {
+ unset($requestedProperties[$addressDataProp]);
+ $val = $node->get();
+ if (is_resource($val))
+ $val = stream_get_contents($val);
+
+ $returnedProperties[200][$addressDataProp] = $val;
+
+ }
+ }
+
+ if ($node instanceof UserAddressBooks) {
+
+ $meCardProp = '{http://calendarserver.org/ns/}me-card';
+ if (in_array($meCardProp, $requestedProperties)) {
+
+ $props = $this->server->getProperties($node->getOwner(), array('{http://sabredav.org/ns}vcard-url'));
+ if (isset($props['{http://sabredav.org/ns}vcard-url'])) {
+
+ $returnedProperties[200][$meCardProp] = new DAV\Property\Href(
+ $props['{http://sabredav.org/ns}vcard-url']
+ );
+ $pos = array_search($meCardProp, $requestedProperties);
+ unset($requestedProperties[$pos]);
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * This event is triggered when a PROPPATCH method is executed
+ *
+ * @param array $mutations
+ * @param array $result
+ * @param DAV\INode $node
+ * @return bool
+ */
+ public function updateProperties(&$mutations, &$result, DAV\INode $node) {
+
+ if (!$node instanceof UserAddressBooks) {
+ return true;
+ }
+
+ $meCard = '{http://calendarserver.org/ns/}me-card';
+
+ // The only property we care about
+ if (!isset($mutations[$meCard]))
+ return true;
+
+ $value = $mutations[$meCard];
+ unset($mutations[$meCard]);
+
+ if ($value instanceof DAV\Property\IHref) {
+ $value = $value->getHref();
+ $value = $this->server->calculateUri($value);
+ } elseif (!is_null($value)) {
+ $result[400][$meCard] = null;
+ return false;
+ }
+
+ $innerResult = $this->server->updateProperties(
+ $node->getOwner(),
+ array(
+ '{http://sabredav.org/ns}vcard-url' => $value,
+ )
+ );
+
+ $closureResult = false;
+ foreach($innerResult as $status => $props) {
+ if (is_array($props) && array_key_exists('{http://sabredav.org/ns}vcard-url', $props)) {
+ $result[$status][$meCard] = null;
+ $closureResult = ($status>=200 && $status<300);
+ }
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * This functions handles REPORT requests specific to CardDAV
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @return bool
+ */
+ public function report($reportName,$dom) {
+
+ switch($reportName) {
+ case '{'.self::NS_CARDDAV.'}addressbook-multiget' :
+ $this->addressbookMultiGetReport($dom);
+ return false;
+ case '{'.self::NS_CARDDAV.'}addressbook-query' :
+ $this->addressBookQueryReport($dom);
+ return false;
+ default :
+ return;
+
+ }
+
+
+ }
+
+ /**
+ * This function handles the addressbook-multiget REPORT.
+ *
+ * This report is used by the client to fetch the content of a series
+ * of urls. Effectively avoiding a lot of redundant requests.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ public function addressbookMultiGetReport($dom) {
+
+ $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
+
+ $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
+ $propertyList = array();
+
+ foreach($hrefElems as $elem) {
+
+ $uri = $this->server->calculateUri($elem->nodeValue);
+ list($propertyList[]) = $this->server->getPropertiesForPath($uri,$properties);
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
+ $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * This method is triggered before a file gets updated with new content.
+ *
+ * This plugin uses this method to ensure that Card nodes receive valid
+ * vcard data.
+ *
+ * @param string $path
+ * @param DAV\IFile $node
+ * @param resource $data
+ * @return void
+ */
+ public function beforeWriteContent($path, DAV\IFile $node, &$data) {
+
+ if (!$node instanceof ICard)
+ return;
+
+ $this->validateVCard($data);
+
+ }
+
+ /**
+ * This method is triggered before a new file is created.
+ *
+ * This plugin uses this method to ensure that Card nodes receive valid
+ * vcard data.
+ *
+ * @param string $path
+ * @param resource $data
+ * @param DAV\ICollection $parentNode
+ * @return void
+ */
+ public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode) {
+
+ if (!$parentNode instanceof IAddressBook)
+ return;
+
+ $this->validateVCard($data);
+
+ }
+
+ /**
+ * Checks if the submitted iCalendar data is in fact, valid.
+ *
+ * An exception is thrown if it's not.
+ *
+ * @param resource|string $data
+ * @return void
+ */
+ protected function validateVCard(&$data) {
+
+ // If it's a stream, we convert it to a string first.
+ if (is_resource($data)) {
+ $data = stream_get_contents($data);
+ }
+
+ // Converting the data to unicode, if needed.
+ $data = DAV\StringUtil::ensureUTF8($data);
+
+ try {
+
+ $vobj = VObject\Reader::read($data);
+
+ } catch (VObject\ParseException $e) {
+
+ throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vcard data. Parse error: ' . $e->getMessage());
+
+ }
+
+ if ($vobj->name !== 'VCARD') {
+ throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.');
+ }
+
+ if (!isset($vobj->UID)) {
+ // No UID in vcards is invalid, but we'll just add it in anyway.
+ $vobj->add('UID', DAV\UUIDUtil::getUUID());
+ $data = $vobj->serialize();
+ }
+
+ }
+
+
+ /**
+ * This function handles the addressbook-query REPORT
+ *
+ * This report is used by the client to filter an addressbook based on a
+ * complex query.
+ *
+ * @param \DOMNode $dom
+ * @return void
+ */
+ protected function addressbookQueryReport($dom) {
+
+ $query = new AddressBookQueryParser($dom);
+ $query->parse();
+
+ $depth = $this->server->getHTTPDepth(0);
+
+ if ($depth==0) {
+ $candidateNodes = array(
+ $this->server->tree->getNodeForPath($this->server->getRequestUri())
+ );
+ } else {
+ $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri());
+ }
+
+ $validNodes = array();
+ foreach($candidateNodes as $node) {
+
+ if (!$node instanceof ICard)
+ continue;
+
+ $blob = $node->get();
+ if (is_resource($blob)) {
+ $blob = stream_get_contents($blob);
+ }
+
+ if (!$this->validateFilters($blob, $query->filters, $query->test)) {
+ continue;
+ }
+
+ $validNodes[] = $node;
+
+ if ($query->limit && $query->limit <= count($validNodes)) {
+ // We hit the maximum number of items, we can stop now.
+ break;
+ }
+
+ }
+
+ $result = array();
+ foreach($validNodes as $validNode) {
+
+ if ($depth==0) {
+ $href = $this->server->getRequestUri();
+ } else {
+ $href = $this->server->getRequestUri() . '/' . $validNode->getName();
+ }
+
+ list($result[]) = $this->server->getPropertiesForPath($href, $query->requestedProperties, 0);
+
+ }
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
+ $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * Validates if a vcard makes it throught a list of filters.
+ *
+ * @param string $vcardData
+ * @param array $filters
+ * @param string $test anyof or allof (which means OR or AND)
+ * @return bool
+ */
+ public function validateFilters($vcardData, array $filters, $test) {
+
+ $vcard = VObject\Reader::read($vcardData);
+
+ if (!$filters) return true;
+
+ foreach($filters as $filter) {
+
+ $isDefined = isset($vcard->{$filter['name']});
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ $success = false;
+ } else {
+ $success = true;
+ }
+ } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) {
+
+ // We only need to check for existence
+ $success = $isDefined;
+
+ } else {
+
+ $vProperties = $vcard->select($filter['name']);
+
+ $results = array();
+ if ($filter['param-filters']) {
+ $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']);
+ }
+ if ($filter['text-matches']) {
+ $texts = array();
+ foreach($vProperties as $vProperty)
+ $texts[] = $vProperty->getValue();
+
+ $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']);
+ }
+
+ if (count($results)===1) {
+ $success = $results[0];
+ } else {
+ if ($filter['test'] === 'anyof') {
+ $success = $results[0] || $results[1];
+ } else {
+ $success = $results[0] && $results[1];
+ }
+ }
+
+ } // else
+
+ // There are two conditions where we can already determine whether
+ // or not this filter succeeds.
+ if ($test==='anyof' && $success) {
+ return true;
+ }
+ if ($test==='allof' && !$success) {
+ return false;
+ }
+
+ } // foreach
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return $test==='allof';
+
+ }
+
+ /**
+ * Validates if a param-filter can be applied to a specific property.
+ *
+ * @todo currently we're only validating the first parameter of the passed
+ * property. Any subsequence parameters with the same name are
+ * ignored.
+ * @param array $vProperties
+ * @param array $filters
+ * @param string $test
+ * @return bool
+ */
+ protected function validateParamFilters(array $vProperties, array $filters, $test) {
+
+ foreach($filters as $filter) {
+
+ $isDefined = false;
+ foreach($vProperties as $vProperty) {
+ $isDefined = isset($vProperty[$filter['name']]);
+ if ($isDefined) break;
+ }
+
+ if ($filter['is-not-defined']) {
+ if ($isDefined) {
+ $success = false;
+ } else {
+ $success = true;
+ }
+
+ // If there's no text-match, we can just check for existence
+ } elseif (!$filter['text-match'] || !$isDefined) {
+
+ $success = $isDefined;
+
+ } else {
+
+ $success = false;
+ foreach($vProperties as $vProperty) {
+ // If we got all the way here, we'll need to validate the
+ // text-match filter.
+ $success = DAV\StringUtil::textMatch($vProperty[$filter['name']]->getValue(), $filter['text-match']['value'], $filter['text-match']['collation'], $filter['text-match']['match-type']);
+ if ($success) break;
+ }
+ if ($filter['text-match']['negate-condition']) {
+ $success = !$success;
+ }
+
+ } // else
+
+ // There are two conditions where we can already determine whether
+ // or not this filter succeeds.
+ if ($test==='anyof' && $success) {
+ return true;
+ }
+ if ($test==='allof' && !$success) {
+ return false;
+ }
+
+ }
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return $test==='allof';
+
+ }
+
+ /**
+ * Validates if a text-filter can be applied to a specific property.
+ *
+ * @param array $texts
+ * @param array $filters
+ * @param string $test
+ * @return bool
+ */
+ protected function validateTextMatches(array $texts, array $filters, $test) {
+
+ foreach($filters as $filter) {
+
+ $success = false;
+ foreach($texts as $haystack) {
+ $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']);
+
+ // Breaking on the first match
+ if ($success) break;
+ }
+ if ($filter['negate-condition']) {
+ $success = !$success;
+ }
+
+ if ($success && $test==='anyof')
+ return true;
+
+ if (!$success && $test=='allof')
+ return false;
+
+
+ }
+
+ // If we got all the way here, it means we haven't been able to
+ // determine early if the test failed or not.
+ //
+ // This implies for 'anyof' that the test failed, and for 'allof' that
+ // we succeeded. Sounds weird, but makes sense.
+ return $test==='allof';
+
+ }
+
+ /**
+ * This event is triggered after webdav-properties have been retrieved.
+ *
+ * @return bool
+ */
+ public function afterGetProperties($uri, &$properties) {
+
+ // If the request was made using the SOGO connector, we must rewrite
+ // the content-type property. By default SabreDAV will send back
+ // text/x-vcard; charset=utf-8, but for SOGO we must strip that last
+ // part.
+ if (!isset($properties[200]['{DAV:}getcontenttype']))
+ return;
+
+ if (strpos($this->server->httpRequest->getHeader('User-Agent'),'Thunderbird')===false) {
+ return;
+ }
+
+ if (strpos($properties[200]['{DAV:}getcontenttype'],'text/x-vcard')===0) {
+ $properties[200]['{DAV:}getcontenttype'] = 'text/x-vcard';
+ }
+
+ }
+
+ /**
+ * This method is used to generate HTML output for the
+ * OldSabre\DAV\Browser\Plugin. This allows us to generate an interface users
+ * can use to create new calendars.
+ *
+ * @param DAV\INode $node
+ * @param string $output
+ * @return bool
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output) {
+
+ if (!$node instanceof UserAddressBooks)
+ return;
+
+ $output.= '<tr><td colspan="2"><form method="post" action="">
+ <h3>Create new address book</h3>
+ <input type="hidden" name="sabreAction" value="mkaddressbook" />
+ <label>Name (uri):</label> <input type="text" name="name" /><br />
+ <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
+ <input type="submit" value="create" />
+ </form>
+ </td></tr>';
+
+ return false;
+
+ }
+
+ /**
+ * This method allows us to intercept the 'mkcalendar' sabreAction. This
+ * action enables the user to create new calendars from the browser plugin.
+ *
+ * @param string $uri
+ * @param string $action
+ * @param array $postVars
+ * @return bool
+ */
+ public function browserPostAction($uri, $action, array $postVars) {
+
+ if ($action!=='mkaddressbook')
+ return;
+
+ $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:carddav}addressbook');
+ $properties = array();
+ if (isset($postVars['{DAV:}displayname'])) {
+ $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
+ }
+ $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
+ return false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php
new file mode 100644
index 0000000..c27c678
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Property/SupportedAddressData.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace OldSabre\CardDAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\CardDAV;
+
+/**
+ * Supported-address-data property
+ *
+ * This property is a representation of the supported-address-data property
+ * in the CardDAV namespace.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedAddressData extends DAV\Property {
+
+ /**
+ * supported versions
+ *
+ * @var array
+ */
+ protected $supportedData = array();
+
+ /**
+ * Creates the property
+ *
+ * @param array|null $supportedData
+ */
+ public function __construct(array $supportedData = null) {
+
+ if (is_null($supportedData)) {
+ $supportedData = array(
+ array('contentType' => 'text/vcard', 'version' => '3.0'),
+ // array('contentType' => 'text/vcard', 'version' => '4.0'),
+ );
+ }
+
+ $this->supportedData = $supportedData;
+
+ }
+
+ /**
+ * Serializes the property in a DOMDocument
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+
+ $prefix =
+ isset($server->xmlNamespaces[CardDAV\Plugin::NS_CARDDAV]) ?
+ $server->xmlNamespaces[CardDAV\Plugin::NS_CARDDAV] :
+ 'card';
+
+ foreach($this->supportedData as $supported) {
+
+ $caldata = $doc->createElementNS(CardDAV\Plugin::NS_CARDDAV, $prefix . ':address-data-type');
+ $caldata->setAttribute('content-type',$supported['contentType']);
+ $caldata->setAttribute('version',$supported['version']);
+ $node->appendChild($caldata);
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php
new file mode 100644
index 0000000..7f3ac6f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/UserAddressBooks.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * UserAddressBooks class
+ *
+ * The UserAddressBooks collection contains a list of addressbooks associated with a user
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UserAddressBooks extends DAV\Collection implements DAV\IExtendedCollection, DAVACL\IACL {
+
+ /**
+ * Principal uri
+ *
+ * @var array
+ */
+ protected $principalUri;
+
+ /**
+ * carddavBackend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $carddavBackend;
+
+ /**
+ * Constructor
+ *
+ * @param Backend\BackendInterface $carddavBackend
+ * @param string $principalUri
+ */
+ public function __construct(Backend\BackendInterface $carddavBackend, $principalUri) {
+
+ $this->carddavBackend = $carddavBackend;
+ $this->principalUri = $principalUri;
+
+ }
+
+ /**
+ * Returns the name of this object
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(,$name) = DAV\URLUtil::splitPath($this->principalUri);
+ return $name;
+
+ }
+
+ /**
+ * Updates the name of this object
+ *
+ * @param string $name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new DAV\Exception\MethodNotAllowed();
+
+ }
+
+ /**
+ * Deletes this object
+ *
+ * @return void
+ */
+ public function delete() {
+
+ throw new DAV\Exception\MethodNotAllowed();
+
+ }
+
+ /**
+ * Returns the last modification date
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return null;
+
+ }
+
+ /**
+ * Creates a new file under this object.
+ *
+ * This is currently not allowed
+ *
+ * @param string $filename
+ * @param resource $data
+ * @return void
+ */
+ public function createFile($filename, $data=null) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+
+ }
+
+ /**
+ * Creates a new directory under this object.
+ *
+ * This is currently not allowed.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function createDirectory($filename) {
+
+ throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+
+ }
+
+ /**
+ * Returns a single calendar, by name
+ *
+ * @param string $name
+ * @todo needs optimizing
+ * @return \AddressBook
+ */
+ public function getChild($name) {
+
+ foreach($this->getChildren() as $child) {
+ if ($name==$child->getName())
+ return $child;
+
+ }
+ throw new DAV\Exception\NotFound('Addressbook with name \'' . $name . '\' could not be found');
+
+ }
+
+ /**
+ * Returns a list of addressbooks
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ $addressbooks = $this->carddavBackend->getAddressbooksForUser($this->principalUri);
+ $objs = array();
+ foreach($addressbooks as $addressbook) {
+ $objs[] = new AddressBook($this->carddavBackend, $addressbook);
+ }
+ return $objs;
+
+ }
+
+ /**
+ * Creates a new addressbook
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ public function createExtendedCollection($name, array $resourceType, array $properties) {
+
+ if (!in_array('{'.Plugin::NS_CARDDAV.'}addressbook',$resourceType) || count($resourceType)!==2) {
+ throw new DAV\Exception\InvalidResourceType('Unknown resourceType for this collection');
+ }
+ $this->carddavBackend->createAddressBook($this->principalUri, $name, $properties);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalUri;
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->principalUri,
+ 'protected' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->principalUri,
+ 'protected' => true,
+ ),
+
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php
new file mode 100644
index 0000000..bfd3790
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/VCFExportPlugin.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+use OldSabre\DAV;
+use OldSabre\VObject;
+
+/**
+ * VCF Exporter
+ *
+ * This plugin adds the ability to export entire address books as .vcf files.
+ * This is useful for clients that don't support CardDAV yet. They often do
+ * support vcf files.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @author Thomas Tanghus (http://tanghus.net/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class VCFExportPlugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and registers event handlers
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90);
+
+ }
+
+ /**
+ * 'beforeMethod' event handles. This event handles intercepts GET requests ending
+ * with ?export
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ if ($method!='GET') return;
+ if ($this->server->httpRequest->getQueryString()!='export') return;
+
+ // splitting uri
+ list($uri) = explode('?',$uri,2);
+
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ if (!($node instanceof IAddressBook)) return;
+
+ // Checking ACL, if available.
+ if ($aclPlugin = $this->server->getPlugin('acl')) {
+ $aclPlugin->checkPrivileges($uri, '{DAV:}read');
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type','text/directory');
+ $this->server->httpResponse->sendStatus(200);
+
+ $nodes = $this->server->getPropertiesForPath($uri, array(
+ '{' . Plugin::NS_CARDDAV . '}address-data',
+ ),1);
+
+ $this->server->httpResponse->sendBody($this->generateVCF($nodes));
+
+ // Returning false to break the event chain
+ return false;
+
+ }
+
+ /**
+ * Merges all vcard objects, and builds one big vcf export
+ *
+ * @param array $nodes
+ * @return string
+ */
+ public function generateVCF(array $nodes) {
+
+ $output = "";
+
+ foreach($nodes as $node) {
+
+ if (!isset($node[200]['{' . Plugin::NS_CARDDAV . '}address-data'])) {
+ continue;
+ }
+ $nodeData = $node[200]['{' . Plugin::NS_CARDDAV . '}address-data'];
+
+ // Parsing this node so VObject can clean up the output.
+ $output .=
+ VObject\Reader::read($nodeData)->serialize();
+
+ }
+
+ return $output;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php
new file mode 100644
index 0000000..9da06a8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/CardDAV/Version.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace OldSabre\CardDAV;
+
+/**
+ * Version Class
+ *
+ * This class contains the OldSabre\CardDAV version information
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php
new file mode 100644
index 0000000..986bc04
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractBasic.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+use OldSabre\DAV;
+use OldSabre\HTTP;
+
+/**
+ * HTTP Basic authentication backend class
+ *
+ * This class can be used by authentication objects wishing to use HTTP Basic
+ * Most of the digest logic is handled, implementors just need to worry about
+ * the validateUserPass method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author James David Low (http://jameslow.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBasic implements BackendInterface {
+
+ /**
+ * This variable holds the currently logged in username.
+ *
+ * @var string|null
+ */
+ protected $currentUser;
+
+ /**
+ * Validates a username and password
+ *
+ * This method should return true or false depending on if login
+ * succeeded.
+ *
+ * @param string $username
+ * @param string $password
+ * @return bool
+ */
+ abstract protected function validateUserPass($username, $password);
+
+ /**
+ * Returns information about the currently logged in username.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+ return $this->currentUser;
+ }
+
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param DAV\Server $server
+ * @param string $realm
+ * @throws DAV\Exception\NotAuthenticated
+ * @return bool
+ */
+ public function authenticate(DAV\Server $server, $realm) {
+
+ $auth = new HTTP\BasicAuth();
+ $auth->setHTTPRequest($server->httpRequest);
+ $auth->setHTTPResponse($server->httpResponse);
+ $auth->setRealm($realm);
+ $userpass = $auth->getUserPass();
+ if (!$userpass) {
+ $auth->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('No basic authentication headers were found');
+ }
+
+ // Authenticates the user
+ if (!$this->validateUserPass($userpass[0],$userpass[1])) {
+ $auth->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('Username or password does not match');
+ }
+ $this->currentUser = $userpass[0];
+ return true;
+ }
+
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php
new file mode 100644
index 0000000..9513493
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/AbstractDigest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+use OldSabre\HTTP;
+use OldSabre\DAV;
+
+/**
+ * HTTP Digest authentication backend class
+ *
+ * This class can be used by authentication objects wishing to use HTTP Digest
+ * Most of the digest logic is handled, implementors just need to worry about
+ * the getDigestHash method
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractDigest implements BackendInterface {
+
+ /**
+ * This variable holds the currently logged in username.
+ *
+ * @var array|null
+ */
+ protected $currentUser;
+
+ /**
+ * Returns a users digest hash based on the username and realm.
+ *
+ * If the user was not known, null must be returned.
+ *
+ * @param string $realm
+ * @param string $username
+ * @return string|null
+ */
+ abstract public function getDigestHash($realm, $username);
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param DAV\Server $server
+ * @param string $realm
+ * @throws DAV\Exception\NotAuthenticated
+ * @return bool
+ */
+ public function authenticate(DAV\Server $server, $realm) {
+
+ $digest = new HTTP\DigestAuth();
+
+ // Hooking up request and response objects
+ $digest->setHTTPRequest($server->httpRequest);
+ $digest->setHTTPResponse($server->httpResponse);
+
+ $digest->setRealm($realm);
+ $digest->init();
+
+ $username = $digest->getUsername();
+
+ // No username was given
+ if (!$username) {
+ $digest->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('No digest authentication headers were found');
+ }
+
+ $hash = $this->getDigestHash($realm, $username);
+ // If this was false, the user account didn't exist
+ if ($hash===false || is_null($hash)) {
+ $digest->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('The supplied username was not on file');
+ }
+ if (!is_string($hash)) {
+ throw new DAV\Exception('The returned value from getDigestHash must be a string or null');
+ }
+
+ // If this was false, the password or part of the hash was incorrect.
+ if (!$digest->validateA1($hash)) {
+ $digest->requireLogin();
+ throw new DAV\Exception\NotAuthenticated('Incorrect username');
+ }
+
+ $this->currentUser = $username;
+ return true;
+
+ }
+
+ /**
+ * Returns the currently logged in username.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+
+ return $this->currentUser;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php
new file mode 100644
index 0000000..79d94af
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/Apache.php
@@ -0,0 +1,63 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+use OldSabre\DAV;
+
+/**
+ * Apache authenticator
+ *
+ * This authentication backend assumes that authentication has been
+ * configured in apache, rather than within SabreDAV.
+ *
+ * Make sure apache is properly configured for this to work.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Apache implements BackendInterface {
+
+ /**
+ * Current apache user
+ *
+ * @var string
+ */
+ protected $remoteUser;
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param DAV\Server $server
+ * @param string $realm
+ * @return bool
+ */
+ public function authenticate(DAV\Server $server, $realm) {
+
+ $remoteUser = $server->httpRequest->getRawServerValue('REMOTE_USER');
+ if (is_null($remoteUser)) {
+ throw new DAV\Exception('We did not receive the $_SERVER[REMOTE_USER] property. This means that apache might have been misconfigured');
+ }
+
+ $this->remoteUser = $remoteUser;
+ return true;
+
+ }
+
+ /**
+ * Returns information about the currently logged in user.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return array|null
+ */
+ public function getCurrentUser() {
+
+ return $this->remoteUser;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php
new file mode 100644
index 0000000..91241ab
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/BackendInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+/**
+ * This is the base class for any authentication object.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Authenticates the user based on the current request.
+ *
+ * If authentication is successful, true must be returned.
+ * If authentication fails, an exception must be thrown.
+ *
+ * @param \OldSabre\DAV\Server $server
+ * @param string $realm
+ * @return bool
+ */
+ function authenticate(\OldSabre\DAV\Server $server,$realm);
+
+ /**
+ * Returns information about the currently logged in username.
+ *
+ * If nobody is currently logged in, this method should return null.
+ *
+ * @return string|null
+ */
+ function getCurrentUser();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php
new file mode 100644
index 0000000..8710832
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/File.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+use OldSabre\DAV;
+
+/**
+ * This is an authentication backend that uses a file to manage passwords.
+ *
+ * The backend file must conform to Apache's htdigest format
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends AbstractDigest {
+
+ /**
+ * List of users
+ *
+ * @var array
+ */
+ protected $users = array();
+
+ /**
+ * Creates the backend object.
+ *
+ * If the filename argument is passed in, it will parse out the specified file fist.
+ *
+ * @param string|null $filename
+ */
+ public function __construct($filename=null) {
+
+ if (!is_null($filename))
+ $this->loadFile($filename);
+
+ }
+
+ /**
+ * Loads an htdigest-formatted file. This method can be called multiple times if
+ * more than 1 file is used.
+ *
+ * @param string $filename
+ * @return void
+ */
+ public function loadFile($filename) {
+
+ foreach(file($filename,FILE_IGNORE_NEW_LINES) as $line) {
+
+ if (substr_count($line, ":") !== 2)
+ throw new DAV\Exception('Malformed htdigest file. Every line should contain 2 colons');
+
+ list($username,$realm,$A1) = explode(':',$line);
+
+ if (!preg_match('/^[a-zA-Z0-9]{32}$/', $A1))
+ throw new DAV\Exception('Malformed htdigest file. Invalid md5 hash');
+
+ $this->users[$realm . ':' . $username] = $A1;
+
+ }
+
+ }
+
+ /**
+ * Returns a users' information
+ *
+ * @param string $realm
+ * @param string $username
+ * @return string
+ */
+ public function getDigestHash($realm, $username) {
+
+ return isset($this->users[$realm . ':' . $username])?$this->users[$realm . ':' . $username]:false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php
new file mode 100644
index 0000000..0b8b2be
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Backend/PDO.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace OldSabre\DAV\Auth\Backend;
+
+/**
+ * This is an authentication backend that uses a file to manage passwords.
+ *
+ * The backend file must conform to Apache's htdigest format
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractDigest {
+
+ /**
+ * Reference to PDO connection
+ *
+ * @var PDO
+ */
+ protected $pdo;
+
+ /**
+ * PDO table name we'll be using
+ *
+ * @var string
+ */
+ protected $tableName;
+
+
+ /**
+ * Creates the backend object.
+ *
+ * If the filename argument is passed in, it will parse out the specified file fist.
+ *
+ * @param PDO $pdo
+ * @param string $tableName The PDO table name to use
+ */
+ public function __construct(\PDO $pdo, $tableName = 'users') {
+
+ $this->pdo = $pdo;
+ $this->tableName = $tableName;
+
+ }
+
+ /**
+ * Returns the digest hash for a user.
+ *
+ * @param string $realm
+ * @param string $username
+ * @return string|null
+ */
+ public function getDigestHash($realm,$username) {
+
+ $stmt = $this->pdo->prepare('SELECT username, digesta1 FROM '.$this->tableName.' WHERE username = ?');
+ $stmt->execute(array($username));
+ $result = $stmt->fetchAll();
+
+ if (!count($result)) return;
+
+ return $result[0]['digesta1'];
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php
new file mode 100644
index 0000000..17106cb
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Auth/Plugin.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace OldSabre\DAV\Auth;
+use OldSabre\DAV;
+
+/**
+ * This plugin provides Authentication for a WebDAV server.
+ *
+ * It relies on a Backend object, which provides user information.
+ *
+ * Additionally, it provides support for:
+ * * {DAV:}current-user-principal property from RFC5397
+ * * {DAV:}principal-collection-set property from RFC3744
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to main server object
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Authentication backend
+ *
+ * @var Backend\BackendInterface
+ */
+ protected $authBackend;
+
+ /**
+ * The authentication realm.
+ *
+ * @var string
+ */
+ private $realm;
+
+ /**
+ * __construct
+ *
+ * @param Backend\BackendInterface $authBackend
+ * @param string $realm
+ */
+ public function __construct(Backend\BackendInterface $authBackend, $realm) {
+
+ $this->authBackend = $authBackend;
+ $this->realm = $realm;
+
+ }
+
+ /**
+ * Initializes the plugin. This function is automatically called by the server
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),10);
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'auth';
+
+ }
+
+ /**
+ * Returns the current users' principal uri.
+ *
+ * If nobody is logged in, this will return null.
+ *
+ * @return string|null
+ */
+ public function getCurrentUser() {
+
+ $userInfo = $this->authBackend->getCurrentUser();
+ if (!$userInfo) return null;
+
+ return $userInfo;
+
+ }
+
+ /**
+ * This method is called before any HTTP method and forces users to be authenticated
+ *
+ * @param string $method
+ * @param string $uri
+ * @throws OldSabre\DAV\Exception\NotAuthenticated
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ $this->authBackend->authenticate($this->server,$this->realm);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php
new file mode 100644
index 0000000..050a243
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/GuessContentType.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace OldSabre\DAV\Browser;
+
+use OldSabre\DAV;
+
+/**
+ * GuessContentType plugin
+ *
+ * A lot of the built-in File objects just return application/octet-stream
+ * as a content-type by default. This is a problem for some clients, because
+ * they expect a correct contenttype.
+ *
+ * There's really no accurate, fast and portable way to determine the contenttype
+ * so this extension does what the rest of the world does, and guesses it based
+ * on the file extension.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class GuessContentType extends DAV\ServerPlugin {
+
+ /**
+ * List of recognized file extensions
+ *
+ * Feel free to add more
+ *
+ * @var array
+ */
+ public $extensionMap = array(
+
+ // images
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'png' => 'image/png',
+
+ // groupware
+ 'ics' => 'text/calendar',
+ 'vcf' => 'text/x-vcard',
+
+ // text
+ 'txt' => 'text/plain',
+
+ );
+
+ /**
+ * Initializes the plugin
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ // Using a relatively low priority (200) to allow other extensions
+ // to set the content-type first.
+ $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'),200);
+
+ }
+
+ /**
+ * Handler for teh afterGetProperties event
+ *
+ * @param string $path
+ * @param array $properties
+ * @return void
+ */
+ public function afterGetProperties($path, &$properties) {
+
+ if (array_key_exists('{DAV:}getcontenttype', $properties[404])) {
+
+ list(, $fileName) = DAV\URLUtil::splitPath($path);
+ $contentType = $this->getContentType($fileName);
+
+ if ($contentType) {
+ $properties[200]['{DAV:}getcontenttype'] = $contentType;
+ unset($properties[404]['{DAV:}getcontenttype']);
+ }
+
+ }
+
+ }
+
+ /**
+ * Simple method to return the contenttype
+ *
+ * @param string $fileName
+ * @return string
+ */
+ protected function getContentType($fileName) {
+
+ // Just grabbing the extension
+ $extension = strtolower(substr($fileName,strrpos($fileName,'.')+1));
+ if (isset($this->extensionMap[$extension]))
+ return $this->extensionMap[$extension];
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php
new file mode 100644
index 0000000..5d6e3c8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/MapGetToPropFind.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace OldSabre\DAV\Browser;
+
+use OldSabre\DAV;
+
+/**
+ * This is a simple plugin that will map any GET request for non-files to
+ * PROPFIND allprops-requests.
+ *
+ * This should allow easy debugging of PROPFIND
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class MapGetToPropFind extends DAV\ServerPlugin {
+
+ /**
+ * reference to server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and subscribes to events
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor'));
+ }
+
+ /**
+ * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function httpGetInterceptor($method, $uri) {
+
+ if ($method!='GET') return true;
+
+ $node = $this->server->tree->getNodeForPath($uri);
+ if ($node instanceof DAV\IFile) return;
+
+ $this->server->invokeMethod('PROPFIND',$uri);
+ return false;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php
new file mode 100644
index 0000000..7a2319b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/Plugin.php
@@ -0,0 +1,491 @@
+<?php
+
+namespace OldSabre\DAV\Browser;
+
+use OldSabre\DAV;
+
+/**
+ * Browser Plugin
+ *
+ * This plugin provides a html representation, so that a WebDAV server may be accessed
+ * using a browser.
+ *
+ * The class intercepts GET requests to collection resources and generates a simple
+ * html index.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * List of default icons for nodes.
+ *
+ * This is an array with class / interface names as keys, and asset names
+ * as values.
+ *
+ * The evaluation order is reversed. The last item in the list gets
+ * precendence.
+ *
+ * @var array
+ */
+ public $iconMap = array(
+ 'OldSabre\\DAV\\IFile' => 'icons/file',
+ 'OldSabre\\DAV\\ICollection' => 'icons/collection',
+ 'OldSabre\\DAVACL\\IPrincipal' => 'icons/principal',
+ 'OldSabre\\CalDAV\\ICalendar' => 'icons/calendar',
+ 'OldSabre\\CardDAV\\IAddressBook' => 'icons/addressbook',
+ 'OldSabre\\CardDAV\\ICard' => 'icons/card',
+ );
+
+ /**
+ * The file extension used for all icons
+ *
+ * @var string
+ */
+ public $iconExtension = '.png';
+
+ /**
+ * reference to server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * enablePost turns on the 'actions' panel, which allows people to create
+ * folders and upload files straight from a browser.
+ *
+ * @var bool
+ */
+ protected $enablePost = true;
+
+ /**
+ * By default the browser plugin will generate a favicon and other images.
+ * To turn this off, set this property to false.
+ *
+ * @var bool
+ */
+ protected $enableAssets = true;
+
+ /**
+ * Creates the object.
+ *
+ * By default it will allow file creation and uploads.
+ * Specify the first argument as false to disable this
+ *
+ * @param bool $enablePost
+ * @param bool $enableAssets
+ */
+ public function __construct($enablePost=true, $enableAssets = true) {
+
+ $this->enablePost = $enablePost;
+ $this->enableAssets = $enableAssets;
+
+ }
+
+ /**
+ * Initializes the plugin and subscribes to events
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'httpGetInterceptor'));
+ $this->server->subscribeEvent('onHTMLActionsPanel', array($this, 'htmlActionsPanel'),200);
+ if ($this->enablePost) $this->server->subscribeEvent('unknownMethod',array($this,'httpPOSTHandler'));
+ }
+
+ /**
+ * This method intercepts GET requests to collections and returns the html
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function httpGetInterceptor($method, $uri) {
+
+ if ($method !== 'GET') return true;
+
+ // We're not using straight-up $_GET, because we want everything to be
+ // unit testable.
+ $getVars = array();
+ parse_str($this->server->httpRequest->getQueryString(), $getVars);
+
+ if (isset($getVars['sabreAction']) && $getVars['sabreAction'] === 'asset' && isset($getVars['assetName'])) {
+ $this->serveAsset($getVars['assetName']);
+ return false;
+ }
+
+ try {
+ $node = $this->server->tree->getNodeForPath($uri);
+ } catch (DAV\Exception\NotFound $e) {
+ // We're simply stopping when the file isn't found to not interfere
+ // with other plugins.
+ return;
+ }
+ if ($node instanceof DAV\IFile)
+ return;
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type','text/html; charset=utf-8');
+
+ $this->server->httpResponse->sendBody(
+ $this->generateDirectoryIndex($uri)
+ );
+
+ return false;
+
+ }
+
+ /**
+ * Handles POST requests for tree operations.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function httpPOSTHandler($method, $uri) {
+
+ if ($method!='POST') return;
+ $contentType = $this->server->httpRequest->getHeader('Content-Type');
+ list($contentType) = explode(';', $contentType);
+ if ($contentType !== 'application/x-www-form-urlencoded' &&
+ $contentType !== 'multipart/form-data') {
+ return;
+ }
+ $postVars = $this->server->httpRequest->getPostVars();
+
+ if (!isset($postVars['sabreAction']))
+ return;
+
+ if ($this->server->broadcastEvent('onBrowserPostAction', array($uri, $postVars['sabreAction'], $postVars))) {
+
+ switch($postVars['sabreAction']) {
+
+ case 'mkcol' :
+ if (isset($postVars['name']) && trim($postVars['name'])) {
+ // Using basename() because we won't allow slashes
+ list(, $folderName) = DAV\URLUtil::splitPath(trim($postVars['name']));
+ $this->server->createDirectory($uri . '/' . $folderName);
+ }
+ break;
+ case 'put' :
+ if ($_FILES) $file = current($_FILES);
+ else break;
+
+ list(, $newName) = DAV\URLUtil::splitPath(trim($file['name']));
+ if (isset($postVars['name']) && trim($postVars['name']))
+ $newName = trim($postVars['name']);
+
+ // Making sure we only have a 'basename' component
+ list(, $newName) = DAV\URLUtil::splitPath($newName);
+
+ if (is_uploaded_file($file['tmp_name'])) {
+ $this->server->createFile($uri . '/' . $newName, fopen($file['tmp_name'],'r'));
+ }
+ break;
+
+ }
+
+ }
+ $this->server->httpResponse->setHeader('Location',$this->server->httpRequest->getUri());
+ $this->server->httpResponse->sendStatus(302);
+ return false;
+
+ }
+
+ /**
+ * Escapes a string for html.
+ *
+ * @param string $value
+ * @return string
+ */
+ public function escapeHTML($value) {
+
+ return htmlspecialchars($value,ENT_QUOTES,'UTF-8');
+
+ }
+
+ /**
+ * Generates the html directory index for a given url
+ *
+ * @param string $path
+ * @return string
+ */
+ public function generateDirectoryIndex($path) {
+
+ $version = '';
+ if (DAV\Server::$exposeVersion) {
+ $version = DAV\Version::VERSION ."-". DAV\Version::STABILITY;
+ }
+
+ $html = "<html>
+<head>
+ <title>Index for " . $this->escapeHTML($path) . "/ - SabreDAV " . $version . "</title>
+ <style type=\"text/css\">
+ body { Font-family: arial}
+ h1 { font-size: 150% }
+ </style>
+ ";
+
+ if ($this->enableAssets) {
+ $html.='<link rel="shortcut icon" href="'.$this->getAssetUrl('favicon.ico').'" type="image/vnd.microsoft.icon" />';
+ }
+
+ $html .= "</head>
+<body>
+ <h1>Index for " . $this->escapeHTML($path) . "/</h1>
+ <table>
+ <tr><th width=\"24\"></th><th>Name</th><th>Type</th><th>Size</th><th>Last modified</th></tr>
+ <tr><td colspan=\"5\"><hr /></td></tr>";
+
+ $files = $this->server->getPropertiesForPath($path,array(
+ '{DAV:}displayname',
+ '{DAV:}resourcetype',
+ '{DAV:}getcontenttype',
+ '{DAV:}getcontentlength',
+ '{DAV:}getlastmodified',
+ ),1);
+
+ $parent = $this->server->tree->getNodeForPath($path);
+
+
+ if ($path) {
+
+ list($parentUri) = DAV\URLUtil::splitPath($path);
+ $fullPath = DAV\URLUtil::encodePath($this->server->getBaseUri() . $parentUri);
+
+ $icon = $this->enableAssets?'<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl('icons/parent' . $this->iconExtension) . '" width="24" alt="Parent" /></a>':'';
+ $html.= "<tr>
+ <td>$icon</td>
+ <td><a href=\"{$fullPath}\">..</a></td>
+ <td>[parent]</td>
+ <td></td>
+ <td></td>
+ </tr>";
+
+ }
+
+ foreach($files as $file) {
+
+ // This is the current directory, we can skip it
+ if (rtrim($file['href'],'/')==$path) continue;
+
+ list(, $name) = DAV\URLUtil::splitPath($file['href']);
+
+ $type = null;
+
+
+ if (isset($file[200]['{DAV:}resourcetype'])) {
+ $type = $file[200]['{DAV:}resourcetype']->getValue();
+
+ // resourcetype can have multiple values
+ if (!is_array($type)) $type = array($type);
+
+ foreach($type as $k=>$v) {
+
+ // Some name mapping is preferred
+ switch($v) {
+ case '{DAV:}collection' :
+ $type[$k] = 'Collection';
+ break;
+ case '{DAV:}principal' :
+ $type[$k] = 'Principal';
+ break;
+ case '{urn:ietf:params:xml:ns:carddav}addressbook' :
+ $type[$k] = 'Addressbook';
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}calendar' :
+ $type[$k] = 'Calendar';
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}schedule-inbox' :
+ $type[$k] = 'Schedule Inbox';
+ break;
+ case '{urn:ietf:params:xml:ns:caldav}schedule-outbox' :
+ $type[$k] = 'Schedule Outbox';
+ break;
+ case '{http://calendarserver.org/ns/}calendar-proxy-read' :
+ $type[$k] = 'Proxy-Read';
+ break;
+ case '{http://calendarserver.org/ns/}calendar-proxy-write' :
+ $type[$k] = 'Proxy-Write';
+ break;
+ }
+
+ }
+ $type = implode(', ', $type);
+ }
+
+ // If no resourcetype was found, we attempt to use
+ // the contenttype property
+ if (!$type && isset($file[200]['{DAV:}getcontenttype'])) {
+ $type = $file[200]['{DAV:}getcontenttype'];
+ }
+ if (!$type) $type = 'Unknown';
+
+ $size = isset($file[200]['{DAV:}getcontentlength'])?(int)$file[200]['{DAV:}getcontentlength']:'';
+ $lastmodified = isset($file[200]['{DAV:}getlastmodified'])?$file[200]['{DAV:}getlastmodified']->getTime()->format(\DateTime::ATOM):'';
+
+ $fullPath = DAV\URLUtil::encodePath('/' . trim($this->server->getBaseUri() . ($path?$path . '/':'') . $name,'/'));
+
+ $displayName = isset($file[200]['{DAV:}displayname'])?$file[200]['{DAV:}displayname']:$name;
+
+ $displayName = $this->escapeHTML($displayName);
+ $type = $this->escapeHTML($type);
+
+ $icon = '';
+
+ if ($this->enableAssets) {
+ $node = $this->server->tree->getNodeForPath(($path?$path.'/':'') . $name);
+ foreach(array_reverse($this->iconMap) as $class=>$iconName) {
+
+ if ($node instanceof $class) {
+ $icon = '<a href="' . $fullPath . '"><img src="' . $this->getAssetUrl($iconName . $this->iconExtension) . '" alt="" width="24" /></a>';
+ break;
+ }
+
+
+ }
+
+ }
+
+ $html.= "<tr>
+ <td>$icon</td>
+ <td><a href=\"{$fullPath}\">{$displayName}</a></td>
+ <td>{$type}</td>
+ <td>{$size}</td>
+ <td>{$lastmodified}</td>
+ </tr>";
+
+ }
+
+ $html.= "<tr><td colspan=\"5\"><hr /></td></tr>";
+
+ $output = '';
+
+ if ($this->enablePost) {
+ $this->server->broadcastEvent('onHTMLActionsPanel',array($parent, &$output));
+ }
+
+ $html.=$output;
+
+ $html.= "</table>
+ <address>Generated by SabreDAV " . $version . " (c)2007-2015 <a href=\"http://sabre.io/\">http://sabre.io/</a></address>
+ </body>
+ </html>";
+
+ return $html;
+
+ }
+
+ /**
+ * This method is used to generate the 'actions panel' output for
+ * collections.
+ *
+ * This specifically generates the interfaces for creating new files, and
+ * creating new directories.
+ *
+ * @param DAV\INode $node
+ * @param mixed $output
+ * @return void
+ */
+ public function htmlActionsPanel(DAV\INode $node, &$output) {
+
+ if (!$node instanceof DAV\ICollection)
+ return;
+
+ // We also know fairly certain that if an object is a non-extended
+ // SimpleCollection, we won't need to show the panel either.
+ if (get_class($node)==='OldSabre\\DAV\\SimpleCollection')
+ return;
+
+ $output.= '<tr><td colspan="2"><form method="post" action="">
+ <h3>Create new folder</h3>
+ <input type="hidden" name="sabreAction" value="mkcol" />
+ Name: <input type="text" name="name" /><br />
+ <input type="submit" value="create" />
+ </form>
+ <form method="post" action="" enctype="multipart/form-data">
+ <h3>Upload file</h3>
+ <input type="hidden" name="sabreAction" value="put" />
+ Name (optional): <input type="text" name="name" /><br />
+ File: <input type="file" name="file" /><br />
+ <input type="submit" value="upload" />
+ </form>
+ </td></tr>';
+
+ }
+
+ /**
+ * This method takes a path/name of an asset and turns it into url
+ * suiteable for http access.
+ *
+ * @param string $assetName
+ * @return string
+ */
+ protected function getAssetUrl($assetName) {
+
+ return $this->server->getBaseUri() . '?sabreAction=asset&assetName=' . urlencode($assetName);
+
+ }
+
+ /**
+ * This method returns a local pathname to an asset.
+ *
+ * @param string $assetName
+ * @return string
+ */
+ protected function getLocalAssetPath($assetName) {
+
+ $assetDir = __DIR__ . '/assets/';
+ $path = $assetDir . $assetName;
+
+ // Making sure people aren't trying to escape from the base path.
+ if (strpos(realpath($path), realpath($assetDir)) === 0) {
+ return $path;
+ }
+ throw new DAV\Exception\Forbidden('Path does not exist, or escaping from the base path was detected');
+ }
+
+ /**
+ * This method reads an asset from disk and generates a full http response.
+ *
+ * @param string $assetName
+ * @return void
+ */
+ protected function serveAsset($assetName) {
+
+ $assetPath = $this->getLocalAssetPath($assetName);
+ if (!file_exists($assetPath)) {
+ throw new DAV\Exception\NotFound('Could not find an asset with this name');
+ }
+ // Rudimentary mime type detection
+ switch(strtolower(substr($assetPath,strpos($assetPath,'.')+1))) {
+
+ case 'ico' :
+ $mime = 'image/vnd.microsoft.icon';
+ break;
+
+ case 'png' :
+ $mime = 'image/png';
+ break;
+
+ default:
+ $mime = 'application/octet-stream';
+ break;
+
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type', $mime);
+ $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath));
+ $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600');
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->sendBody(fopen($assetPath,'r'));
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico
new file mode 100644
index 0000000..2b2c10a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/favicon.ico
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png
new file mode 100644
index 0000000..c9acc84
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/addressbook.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png
new file mode 100644
index 0000000..3ecd6a8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/calendar.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png
new file mode 100644
index 0000000..2ce9548
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/card.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png
new file mode 100644
index 0000000..156fa64
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/collection.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png
new file mode 100644
index 0000000..3b98551
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/file.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png
new file mode 100644
index 0000000..156fa64
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/parent.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png
new file mode 100644
index 0000000..f8988f8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Browser/assets/icons/principal.png
Binary files differ
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php
new file mode 100644
index 0000000..22a44ba
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Client.php
@@ -0,0 +1,578 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * SabreDAV DAV client
+ *
+ * This client wraps around Curl to provide a convenient API to a WebDAV
+ * server.
+ *
+ * NOTE: This class is experimental, it's api will likely change in the future.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Client {
+
+ /**
+ * The propertyMap is a key-value array.
+ *
+ * If you use the propertyMap, any {DAV:}multistatus responses with the
+ * proeprties listed in this array, will automatically be mapped to a
+ * respective class.
+ *
+ * The {DAV:}resourcetype property is automatically added. This maps to
+ * OldSabre\DAV\Property\ResourceType
+ *
+ * @var array
+ */
+ public $propertyMap = array();
+
+ protected $baseUri;
+ protected $userName;
+ protected $password;
+ protected $proxy;
+ protected $trustedCertificates;
+
+ /**
+ * Basic authentication
+ */
+ const AUTH_BASIC = 1;
+
+ /**
+ * Digest authentication
+ */
+ const AUTH_DIGEST = 2;
+
+ /**
+ * The authentication type we're using.
+ *
+ * This is a bitmask of AUTH_BASIC and AUTH_DIGEST.
+ *
+ * If DIGEST is used, the client makes 1 extra request per request, to get
+ * the authentication tokens.
+ *
+ * @var int
+ */
+ protected $authType;
+
+ /**
+ * Indicates if SSL verification is enabled or not.
+ *
+ * @var boolean
+ */
+ protected $verifyPeer;
+
+ /**
+ * Constructor
+ *
+ * Settings are provided through the 'settings' argument. The following
+ * settings are supported:
+ *
+ * * baseUri
+ * * userName (optional)
+ * * password (optional)
+ * * proxy (optional)
+ *
+ * @param array $settings
+ */
+ public function __construct(array $settings) {
+
+ if (!isset($settings['baseUri'])) {
+ throw new \InvalidArgumentException('A baseUri must be provided');
+ }
+
+ $validSettings = array(
+ 'baseUri',
+ 'userName',
+ 'password',
+ 'proxy',
+ );
+
+ foreach($validSettings as $validSetting) {
+ if (isset($settings[$validSetting])) {
+ $this->$validSetting = $settings[$validSetting];
+ }
+ }
+
+ if (isset($settings['authType'])) {
+ $this->authType = $settings['authType'];
+ } else {
+ $this->authType = self::AUTH_BASIC | self::AUTH_DIGEST;
+ }
+
+ $this->propertyMap['{DAV:}resourcetype'] = 'OldSabre\\DAV\\Property\\ResourceType';
+
+ }
+
+ /**
+ * Add trusted root certificates to the webdav client.
+ *
+ * The parameter certificates should be a absolute path to a file
+ * which contains all trusted certificates
+ *
+ * @param string $certificates
+ */
+ public function addTrustedCertificates($certificates) {
+ $this->trustedCertificates = $certificates;
+ }
+
+ /**
+ * Enables/disables SSL peer verification
+ *
+ * @param boolean $value
+ */
+ public function setVerifyPeer($value) {
+ $this->verifyPeer = $value;
+ }
+
+ /**
+ * Does a PROPFIND request
+ *
+ * The list of requested properties must be specified as an array, in clark
+ * notation.
+ *
+ * The returned array will contain a list of filenames as keys, and
+ * properties as values.
+ *
+ * The properties array will contain the list of properties. Only properties
+ * that are actually returned from the server (without error) will be
+ * returned, anything else is discarded.
+ *
+ * Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ *
+ * @param string $url
+ * @param array $properties
+ * @param int $depth
+ * @return array
+ */
+ public function propFind($url, array $properties, $depth = 0) {
+
+ $body = '<?xml version="1.0"?>' . "\n";
+ $body.= '<d:propfind xmlns:d="DAV:">' . "\n";
+ $body.= ' <d:prop>' . "\n";
+
+ foreach($properties as $property) {
+
+ list(
+ $namespace,
+ $elementName
+ ) = XMLUtil::parseClarkNotation($property);
+
+ if ($namespace === 'DAV:') {
+ $body.=' <d:' . $elementName . ' />' . "\n";
+ } else {
+ $body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
+ }
+
+ }
+
+ $body.= ' </d:prop>' . "\n";
+ $body.= '</d:propfind>';
+
+ $response = $this->request('PROPFIND', $url, $body, array(
+ 'Depth' => $depth,
+ 'Content-Type' => 'application/xml'
+ ));
+
+ $result = $this->parseMultiStatus($response['body']);
+
+ // If depth was 0, we only return the top item
+ if ($depth===0) {
+ reset($result);
+ $result = current($result);
+ return isset($result[200])?$result[200]:array();
+ }
+
+ $newResult = array();
+ foreach($result as $href => $statusList) {
+
+ $newResult[$href] = isset($statusList[200])?$statusList[200]:array();
+
+ }
+
+ return $newResult;
+
+ }
+
+ /**
+ * Updates a list of properties on the server
+ *
+ * The list of properties must have clark-notation properties for the keys,
+ * and the actual (string) value for the value. If the value is null, an
+ * attempt is made to delete the property.
+ *
+ * @todo Must be building the request using the DOM, and does not yet
+ * support complex properties.
+ * @param string $url
+ * @param array $properties
+ * @return void
+ */
+ public function propPatch($url, array $properties) {
+
+ $body = '<?xml version="1.0"?>' . "\n";
+ $body.= '<d:propertyupdate xmlns:d="DAV:">' . "\n";
+
+ foreach($properties as $propName => $propValue) {
+
+ list(
+ $namespace,
+ $elementName
+ ) = XMLUtil::parseClarkNotation($propName);
+
+ if ($propValue === null) {
+
+ $body.="<d:remove><d:prop>\n";
+
+ if ($namespace === 'DAV:') {
+ $body.=' <d:' . $elementName . ' />' . "\n";
+ } else {
+ $body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\"/>\n";
+ }
+
+ $body.="</d:prop></d:remove>\n";
+
+ } else {
+
+ $body.="<d:set><d:prop>\n";
+ if ($namespace === 'DAV:') {
+ $body.=' <d:' . $elementName . '>';
+ } else {
+ $body.=" <x:" . $elementName . " xmlns:x=\"" . $namespace . "\">";
+ }
+ // Shitty.. i know
+ $body.=htmlspecialchars($propValue, ENT_NOQUOTES, 'UTF-8');
+ if ($namespace === 'DAV:') {
+ $body.='</d:' . $elementName . '>' . "\n";
+ } else {
+ $body.="</x:" . $elementName . ">\n";
+ }
+ $body.="</d:prop></d:set>\n";
+
+ }
+
+ }
+
+ $body.= '</d:propertyupdate>';
+
+ $this->request('PROPPATCH', $url, $body, array(
+ 'Content-Type' => 'application/xml'
+ ));
+
+ }
+
+ /**
+ * Performs an HTTP options request
+ *
+ * This method returns all the features from the 'DAV:' header as an array.
+ * If there was no DAV header, or no contents this method will return an
+ * empty array.
+ *
+ * @return array
+ */
+ public function options() {
+
+ $result = $this->request('OPTIONS');
+ if (!isset($result['headers']['dav'])) {
+ return array();
+ }
+
+ $features = explode(',', $result['headers']['dav']);
+ foreach($features as &$v) {
+ $v = trim($v);
+ }
+ return $features;
+
+ }
+
+ /**
+ * Performs an actual HTTP request, and returns the result.
+ *
+ * If the specified url is relative, it will be expanded based on the base
+ * url.
+ *
+ * The returned array contains 3 keys:
+ * * body - the response body
+ * * httpCode - a HTTP code (200, 404, etc)
+ * * headers - a list of response http headers. The header names have
+ * been lowercased.
+ *
+ * @param string $method
+ * @param string $url
+ * @param string $body
+ * @param array $headers
+ * @return array
+ */
+ public function request($method, $url = '', $body = null, $headers = array()) {
+
+ $url = $this->getAbsoluteUrl($url);
+
+ $curlSettings = array(
+ CURLOPT_RETURNTRANSFER => true,
+ // Return headers as part of the response
+ CURLOPT_HEADER => true,
+
+ // For security we cast this to a string. If somehow an array could
+ // be passed here, it would be possible for an attacker to use @ to
+ // post local files.
+ CURLOPT_POSTFIELDS => (string)$body,
+ // Automatically follow redirects
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 5,
+ CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
+ CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS
+ );
+
+ if($this->verifyPeer !== null) {
+ $curlSettings[CURLOPT_SSL_VERIFYPEER] = $this->verifyPeer;
+ $curlSettings[CURLOPT_SSL_VERIFYHOST] = $this->verifyPeer;
+ }
+
+ if($this->trustedCertificates) {
+ $curlSettings[CURLOPT_CAINFO] = $this->trustedCertificates;
+ }
+
+ switch ($method) {
+ case 'HEAD' :
+
+ // do not read body with HEAD requests (this is necessary because cURL does not ignore the body with HEAD
+ // requests when the Content-Length header is given - which in turn is perfectly valid according to HTTP
+ // specs...) cURL does unfortunately return an error in this case ("transfer closed transfer closed with
+ // ... bytes remaining to read") this can be circumvented by explicitly telling cURL to ignore the
+ // response body
+ $curlSettings[CURLOPT_NOBODY] = true;
+ $curlSettings[CURLOPT_CUSTOMREQUEST] = 'HEAD';
+ break;
+
+ default:
+ $curlSettings[CURLOPT_CUSTOMREQUEST] = $method;
+ break;
+
+ }
+
+ // Adding HTTP headers
+ $nHeaders = array();
+ foreach($headers as $key=>$value) {
+
+ $nHeaders[] = $key . ': ' . $value;
+
+ }
+ $curlSettings[CURLOPT_HTTPHEADER] = $nHeaders;
+
+ if ($this->proxy) {
+ $curlSettings[CURLOPT_PROXY] = $this->proxy;
+ }
+
+ if ($this->userName && $this->authType) {
+ $curlType = 0;
+ if ($this->authType & self::AUTH_BASIC) {
+ $curlType |= CURLAUTH_BASIC;
+ }
+ if ($this->authType & self::AUTH_DIGEST) {
+ $curlType |= CURLAUTH_DIGEST;
+ }
+ $curlSettings[CURLOPT_HTTPAUTH] = $curlType;
+ $curlSettings[CURLOPT_USERPWD] = $this->userName . ':' . $this->password;
+ }
+
+ list(
+ $response,
+ $curlInfo,
+ $curlErrNo,
+ $curlError
+ ) = $this->curlRequest($url, $curlSettings);
+
+ $headerBlob = substr($response, 0, $curlInfo['header_size']);
+ $response = substr($response, $curlInfo['header_size']);
+
+ // In the case of 100 Continue, or redirects we'll have multiple lists
+ // of headers for each separate HTTP response. We can easily split this
+ // because they are separated by \r\n\r\n
+ $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n"));
+
+ // We only care about the last set of headers
+ $headerBlob = $headerBlob[count($headerBlob)-1];
+
+ // Splitting headers
+ $headerBlob = explode("\r\n", $headerBlob);
+
+ $headers = array();
+ foreach($headerBlob as $header) {
+ $parts = explode(':', $header, 2);
+ if (count($parts)==2) {
+ $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
+ }
+ }
+
+ $response = array(
+ 'body' => $response,
+ 'statusCode' => $curlInfo['http_code'],
+ 'headers' => $headers
+ );
+
+ if ($curlErrNo) {
+ throw new Exception('[CURL] Error while making request: ' . $curlError . ' (error code: ' . $curlErrNo . ')');
+ }
+
+ if ($response['statusCode']>=400) {
+ switch ($response['statusCode']) {
+ case 400 :
+ throw new Exception\BadRequest('Bad request');
+ case 401 :
+ throw new Exception\NotAuthenticated('Not authenticated');
+ case 402 :
+ throw new Exception\PaymentRequired('Payment required');
+ case 403 :
+ throw new Exception\Forbidden('Forbidden');
+ case 404:
+ throw new Exception\NotFound('Resource not found.');
+ case 405 :
+ throw new Exception\MethodNotAllowed('Method not allowed');
+ case 409 :
+ throw new Exception\Conflict('Conflict');
+ case 412 :
+ throw new Exception\PreconditionFailed('Precondition failed');
+ case 416 :
+ throw new Exception\RequestedRangeNotSatisfiable('Requested Range Not Satisfiable');
+ case 500 :
+ throw new Exception('Internal server error');
+ case 501 :
+ throw new Exception\NotImplemented('Not Implemented');
+ case 507 :
+ throw new Exception\InsufficientStorage('Insufficient storage');
+ default:
+ throw new Exception('HTTP error response. (errorcode ' . $response['statusCode'] . ')');
+ }
+ }
+
+ return $response;
+
+ }
+
+ /**
+ * Wrapper for all curl functions.
+ *
+ * The only reason this was split out in a separate method, is so it
+ * becomes easier to unittest.
+ *
+ * @param string $url
+ * @param array $settings
+ * @return array
+ */
+ // @codeCoverageIgnoreStart
+ protected function curlRequest($url, $settings) {
+
+ $curl = curl_init($url);
+ curl_setopt_array($curl, $settings);
+
+ return array(
+ curl_exec($curl),
+ curl_getinfo($curl),
+ curl_errno($curl),
+ curl_error($curl)
+ );
+
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Returns the full url based on the given url (which may be relative). All
+ * urls are expanded based on the base url as given by the server.
+ *
+ * @param string $url
+ * @return string
+ */
+ protected function getAbsoluteUrl($url) {
+
+ // If the url starts with http:// or https://, the url is already absolute.
+ if (preg_match('/^http(s?):\/\//', $url)) {
+ return $url;
+ }
+
+ // If the url starts with a slash, we must calculate the url based off
+ // the root of the base url.
+ if (strpos($url,'/') === 0) {
+ $parts = parse_url($this->baseUri);
+ return $parts['scheme'] . '://' . $parts['host'] . (isset($parts['port'])?':' . $parts['port']:'') . $url;
+ }
+
+ // Otherwise...
+ return $this->baseUri . $url;
+
+ }
+
+ /**
+ * Parses a WebDAV multistatus response body
+ *
+ * This method returns an array with the following structure
+ *
+ * array(
+ * 'url/to/resource' => array(
+ * '200' => array(
+ * '{DAV:}property1' => 'value1',
+ * '{DAV:}property2' => 'value2',
+ * ),
+ * '404' => array(
+ * '{DAV:}property1' => null,
+ * '{DAV:}property2' => null,
+ * ),
+ * )
+ * 'url/to/resource2' => array(
+ * .. etc ..
+ * )
+ * )
+ *
+ *
+ * @param string $body xml body
+ * @return array
+ */
+ public function parseMultiStatus($body) {
+
+ $body = XMLUtil::convertDAVNamespace($body);
+
+ // Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
+ // 5.4.13.
+ $previous = libxml_disable_entity_loader(true);
+ $responseXML = simplexml_load_string($body, null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
+ libxml_disable_entity_loader($previous);
+
+ if ($responseXML===false) {
+ throw new \InvalidArgumentException('The passed data is not valid XML');
+ }
+
+ $responseXML->registerXPathNamespace('d', 'urn:DAV');
+
+ $propResult = array();
+
+ foreach($responseXML->xpath('d:response') as $response) {
+ $response->registerXPathNamespace('d', 'urn:DAV');
+ $href = $response->xpath('d:href');
+ $href = (string)$href[0];
+
+ $properties = array();
+
+ foreach($response->xpath('d:propstat') as $propStat) {
+
+ $propStat->registerXPathNamespace('d', 'urn:DAV');
+ $status = $propStat->xpath('d:status');
+ list($httpVersion, $statusCode, $message) = explode(' ', (string)$status[0],3);
+
+ // Only using the propertymap for results with status 200.
+ $propertyMap = $statusCode==='200' ? $this->propertyMap : array();
+
+ $properties[$statusCode] = XMLUtil::parseProperties(dom_import_simplexml($propStat), $propertyMap);
+
+ }
+
+ $propResult[$href] = $properties;
+
+ }
+
+ return $propResult;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php
new file mode 100644
index 0000000..855667e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Collection.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Collection class
+ *
+ * This is a helper class, that should aid in getting collections classes setup.
+ * Most of its methods are implemented, and throw permission denied exceptions
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Collection extends Node implements ICollection {
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * This method makes use of the getChildren method to grab all the child
+ * nodes, and compares the name.
+ * Generally its wise to override this, as this can usually be optimized
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws Exception\NotFound
+ * @return INode
+ */
+ public function getChild($name) {
+
+ foreach($this->getChildren() as $child) {
+
+ if ($child->getName()==$name) return $child;
+
+ }
+ throw new Exception\NotFound('File not found: ' . $name);
+
+ }
+
+ /**
+ * Checks is a child-node exists.
+ *
+ * It is generally a good idea to try and override this. Usually it can be optimized.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ try {
+
+ $this->getChild($name);
+ return true;
+
+ } catch(Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After succesful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null) {
+
+ throw new Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws Exception\Forbidden
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ throw new Exception\Forbidden('Permission denied to create directory');
+
+ }
+
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php
new file mode 100644
index 0000000..357e818
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SabreDAV base exception
+ *
+ * This is SabreDAV's base exception file, use this to implement your own exception.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+
+namespace OldSabre\DAV;
+
+/**
+ * Main Exception class.
+ *
+ * This class defines a getHTTPCode method, which should return the appropriate HTTP code for the Exception occurred.
+ * The default for this is 500.
+ *
+ * This class also allows you to generate custom xml data for your exceptions. This will be displayed
+ * in the 'error' element in the failing response.
+ */
+class Exception extends \Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 500;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(Server $server,\DOMElement $errorNode) {
+
+
+ }
+
+ /**
+ * This method allows the exception to return any extra HTTP response headers.
+ *
+ * The headers must be returned as an array.
+ *
+ * @param Server $server
+ * @return array
+ */
+ public function getHTTPHeaders(Server $server) {
+
+ return array();
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php
new file mode 100644
index 0000000..aeb6350
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/BadRequest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * BadRequest
+ *
+ * The BadRequest is thrown when the user submitted an invalid HTTP request
+ * BadRequest
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class BadRequest extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 400;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php
new file mode 100644
index 0000000..97ef2cd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Conflict.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * Conflict
+ *
+ * A 409 Conflict is thrown when a user tried to make a directory over an existing
+ * file or in a parent directory that doesn't exist.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Conflict extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 409;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php
new file mode 100644
index 0000000..33839d3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ConflictingLock.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * ConflictingLock
+ *
+ * Similar to the Locked exception, this exception thrown when a LOCK request
+ * was made, on a resource which was already locked
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ConflictingLock extends Locked {
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+ if ($this->lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:no-conflicting-lock');
+ $errorNode->appendChild($error);
+ if (!is_object($this->lock)) var_dump($this->lock);
+ $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:','d:href',$this->lock->uri));
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php
new file mode 100644
index 0000000..f35f9b3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/FileNotFound.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * FileNotFound
+ *
+ * Deprecated: Warning, this class is deprecated and will be removed in a
+ * future version of SabreDAV. Please use OldSabre\DAV\Exception\NotFound instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @deprecated Use OldSabre\DAV\Exception\NotFound instead
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class FileNotFound extends NotFound {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php
new file mode 100644
index 0000000..003d86b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Forbidden.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * Forbidden
+ *
+ * This exception is thrown whenever a user tries to do an operation he's not allowed to
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Forbidden extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 403;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php
new file mode 100644
index 0000000..081a3e4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InsufficientStorage.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * InsufficientStorage
+ *
+ * This Exception can be thrown, when for example a harddisk is full or a quota is exceeded
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InsufficientStorage extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 507;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php
new file mode 100644
index 0000000..9526b8d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/InvalidResourceType.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * InvalidResourceType
+ *
+ * This exception is thrown when the user tried to create a new collection, with
+ * a special resourcetype value that was not recognized by the server.
+ *
+ * See RFC5689 section 3.3
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InvalidResourceType extends Forbidden {
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(\OldSabre\DAV\Server $server,\DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:valid-resourcetype');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php
new file mode 100644
index 0000000..39cd639
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LengthRequired.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * LengthRequired
+ *
+ * This exception is thrown when a request was made that required a
+ * Content-Length header, but did not contain one.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LengthRequired extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 411;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php
new file mode 100644
index 0000000..08d94b5
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/LockTokenMatchesRequestUri.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * LockTokenMatchesRequestUri
+ *
+ * This exception is thrown by UNLOCK if a supplied lock-token is invalid
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LockTokenMatchesRequestUri extends Conflict {
+
+ /**
+ * Creates the exception
+ */
+ public function __construct() {
+
+ $this->message = 'The locktoken supplied does not match any locks on this entity';
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:lock-token-matches-request-uri');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php
new file mode 100644
index 0000000..4d38357
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/Locked.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * Locked
+ *
+ * The 423 is thrown when a client tried to access a resource that was locked, without supplying a valid lock token
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Locked extends DAV\Exception {
+
+ /**
+ * Lock information
+ *
+ * @var OldSabre\DAV\Locks\LockInfo
+ */
+ protected $lock;
+
+ /**
+ * Creates the exception
+ *
+ * A LockInfo object should be passed if the user should be informed
+ * which lock actually has the file locked.
+ *
+ * @param DAV\Locks\LockInfo $lock
+ */
+ public function __construct(DAV\Locks\LockInfo $lock = null) {
+
+ $this->lock = $lock;
+
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 423;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ if ($this->lock) {
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:lock-token-submitted');
+ $errorNode->appendChild($error);
+
+ $href = $errorNode->ownerDocument->createElementNS('DAV:','d:href');
+ $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri));
+ $error->appendChild(
+ $href
+ );
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php
new file mode 100644
index 0000000..adf91e2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/MethodNotAllowed.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * MethodNotAllowed
+ *
+ * The 405 is thrown when a client tried to create a directory on an already existing directory
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class MethodNotAllowed extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 405;
+
+ }
+
+ /**
+ * This method allows the exception to return any extra HTTP response headers.
+ *
+ * The headers must be returned as an array.
+ *
+ * @param \OldSabre\DAV\Server $server
+ * @return array
+ */
+ public function getHTTPHeaders(\OldSabre\DAV\Server $server) {
+
+ $methods = $server->getAllowedMethods($server->getRequestUri());
+
+ return array(
+ 'Allow' => strtoupper(implode(', ',$methods)),
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php
new file mode 100644
index 0000000..2e98802
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotAuthenticated.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * NotAuthenticated
+ *
+ * This exception is thrown when the client did not provide valid
+ * authentication credentials.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotAuthenticated extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 401;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php
new file mode 100644
index 0000000..210f21f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotFound.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * NotFound
+ *
+ * This Exception is thrown when a Node couldn't be found. It returns HTTP error code 404
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotFound extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 404;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php
new file mode 100644
index 0000000..f9624e8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/NotImplemented.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * NotImplemented
+ *
+ * This exception is thrown when the client tried to call an unsupported HTTP method or other feature
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotImplemented extends \OldSabre\DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 501;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php
new file mode 100644
index 0000000..1fd18be
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PaymentRequired.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * Payment Required
+ *
+ * The PaymentRequired exception may be thrown in a case where a user must pay
+ * to access a certain resource or operation.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PaymentRequired extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 402;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php
new file mode 100644
index 0000000..625a7fc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/PreconditionFailed.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * PreconditionFailed
+ *
+ * This exception is normally thrown when a client submitted a conditional request,
+ * like for example an If, If-None-Match or If-Match header, which caused the HTTP
+ * request to not execute (the condition of the header failed)
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PreconditionFailed extends DAV\Exception {
+
+ /**
+ * When this exception is thrown, the header-name might be set.
+ *
+ * This allows the exception-catching code to determine which HTTP header
+ * caused the exception.
+ *
+ * @var string
+ */
+ public $header = null;
+
+ /**
+ * Create the exception
+ *
+ * @param string $message
+ * @param string $header
+ */
+ public function __construct($message, $header=null) {
+
+ parent::__construct($message);
+ $this->header = $header;
+
+ }
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 412;
+
+ }
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ if ($this->header) {
+ $prop = $errorNode->ownerDocument->createElement('s:header');
+ $prop->nodeValue = $this->header;
+ $errorNode->appendChild($prop);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php
new file mode 100644
index 0000000..f1d42ba
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ReportNotSupported.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * ReportNotSupported
+ *
+ * This exception is thrown when the client requested an unknown report through the REPORT method
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ReportNotSupported extends Forbidden {
+
+ /**
+ * This method allows the exception to include additional information into the WebDAV error response
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $error = $errorNode->ownerDocument->createElementNS('DAV:','d:supported-report');
+ $errorNode->appendChild($error);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php
new file mode 100644
index 0000000..5528cde
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/RequestedRangeNotSatisfiable.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * RequestedRangeNotSatisfiable
+ *
+ * This exception is normally thrown when the user
+ * request a range that is out of the entity bounds.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class RequestedRangeNotSatisfiable extends DAV\Exception {
+
+ /**
+ * returns the http statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 416;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php
new file mode 100644
index 0000000..94993f2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/ServiceUnavailable.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * ServiceUnavailable
+ *
+ * This exception is thrown in case the service
+ * is currently not available (e.g. down for maintenance).
+ *
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ServiceUnavailable extends DAV\Exception {
+
+ /**
+ * Returns the HTTP statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 503;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php
new file mode 100644
index 0000000..32ba6f1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Exception/UnsupportedMediaType.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV\Exception;
+
+/**
+ * UnSupportedMediaType
+ *
+ * The 415 Unsupported Media Type status code is generally sent back when the client
+ * tried to call an HTTP method, with a body the server didn't understand
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UnsupportedMediaType extends \OldSabre\DAV\Exception {
+
+ /**
+ * returns the http statuscode for this exception
+ *
+ * @return int
+ */
+ public function getHTTPCode() {
+
+ return 415;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php
new file mode 100644
index 0000000..a8021cc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Directory.php
@@ -0,0 +1,140 @@
+<?php
+
+namespace OldSabre\DAV\FS;
+use OldSabre\DAV;
+
+/**
+ * Directory class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Directory extends Node implements DAV\ICollection, DAV\IQuota {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null) {
+
+ $newPath = $this->path . '/' . $name;
+ file_put_contents($newPath,$data);
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ $newPath = $this->path . '/' . $name;
+ mkdir($newPath);
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return DAV\INode
+ */
+ public function getChild($name) {
+
+ $path = $this->path . '/' . $name;
+
+ if (!file_exists($path)) throw new DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
+
+ if (is_dir($path)) {
+
+ return new Directory($path);
+
+ } else {
+
+ return new File($path);
+
+ }
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren() {
+
+ $nodes = array();
+ foreach(scandir($this->path) as $node) if($node!='.' && $node!='..') $nodes[] = $this->getChild($node);
+ return $nodes;
+
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ $path = $this->path . '/' . $name;
+ return file_exists($path);
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return void
+ */
+ public function delete() {
+
+ foreach($this->getChildren() as $child) $child->delete();
+ rmdir($this->path);
+
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ public function getQuotaInfo() {
+
+ return array(
+ disk_total_space($this->path)-disk_free_space($this->path),
+ disk_free_space($this->path)
+ );
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php
new file mode 100644
index 0000000..e58a705
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/File.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace OldSabre\DAV\FS;
+
+use OldSabre\DAV;
+
+/**
+ * File class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends Node implements DAV\IFile {
+
+ /**
+ * Updates the data
+ *
+ * @param resource $data
+ * @return void
+ */
+ public function put($data) {
+
+ file_put_contents($this->path,$data);
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return string
+ */
+ public function get() {
+
+ return fopen($this->path,'r');
+
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return void
+ */
+ public function delete() {
+
+ unlink($this->path);
+
+ }
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return filesize($this->path);
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return mixed
+ */
+ public function getETag() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ public function getContentType() {
+
+ return null;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php
new file mode 100644
index 0000000..e2bf024
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FS/Node.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace OldSabre\DAV\FS;
+
+use OldSabre\DAV;
+
+/**
+ * Base node-class
+ *
+ * The node class implements the method used by both the File and the Directory classes
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node implements DAV\INode {
+
+ /**
+ * The path to the current node
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Sets up the node, expects a full path name
+ *
+ * @param string $path
+ */
+ public function __construct($path) {
+
+ $this->path = $path;
+
+ }
+
+
+
+ /**
+ * Returns the name of the node
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(, $name) = DAV\URLUtil::splitPath($this->path);
+ return $name;
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ list($parentPath, ) = DAV\URLUtil::splitPath($this->path);
+ list(, $newName) = DAV\URLUtil::splitPath($name);
+
+ $newPath = $parentPath . '/' . $newName;
+ rename($this->path,$newPath);
+
+ $this->path = $newPath;
+
+ }
+
+
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return filemtime($this->path);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php
new file mode 100644
index 0000000..d0e899e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Directory.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace OldSabre\DAV\FSExt;
+
+use OldSabre\DAV;
+
+/**
+ * Directory class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Directory extends Node implements DAV\ICollection, DAV\IQuota {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null) {
+
+ // We're not allowing dots
+ if ($name=='.' || $name=='..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ $newPath = $this->path . '/' . $name;
+ file_put_contents($newPath,$data);
+
+ return '"' . md5_file($newPath) . '"';
+
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ public function createDirectory($name) {
+
+ // We're not allowing dots
+ if ($name=='.' || $name=='..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+ $newPath = $this->path . '/' . $name;
+ mkdir($newPath);
+
+ }
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return DAV\INode
+ */
+ public function getChild($name) {
+
+ $path = $this->path . '/' . $name;
+
+ if (!file_exists($path)) throw new DAV\Exception\NotFound('File could not be located');
+ if ($name=='.' || $name=='..') throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+
+ if (is_dir($path)) {
+
+ return new Directory($path);
+
+ } else {
+
+ return new File($path);
+
+ }
+
+ }
+
+ /**
+ * Checks if a child exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name) {
+
+ if ($name=='.' || $name=='..')
+ throw new DAV\Exception\Forbidden('Permission denied to . and ..');
+
+ $path = $this->path . '/' . $name;
+ return file_exists($path);
+
+ }
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ public function getChildren() {
+
+ $nodes = array();
+ foreach(scandir($this->path) as $node) if($node!='.' && $node!='..' && $node!='.sabredav') $nodes[] = $this->getChild($node);
+ return $nodes;
+
+ }
+
+ /**
+ * Deletes all files in this directory, and then itself
+ *
+ * @return bool
+ */
+ public function delete() {
+
+ // Deleting all children
+ foreach($this->getChildren() as $child) $child->delete();
+
+ // Removing resource info, if its still around
+ if (file_exists($this->path . '/.sabredav')) unlink($this->path . '/.sabredav');
+
+ // Removing the directory itself
+ rmdir($this->path);
+
+ return parent::delete();
+
+ }
+
+ /**
+ * Returns available diskspace information
+ *
+ * @return array
+ */
+ public function getQuotaInfo() {
+
+ return array(
+ disk_total_space($this->path)-disk_free_space($this->path),
+ disk_free_space($this->path)
+ );
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php
new file mode 100644
index 0000000..d42e486
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/File.php
@@ -0,0 +1,146 @@
+<?php
+
+namespace OldSabre\DAV\FSExt;
+use OldSabre\DAV;
+
+/**
+ * File class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends Node implements DAV\PartialUpdate\IPatchSupport {
+
+ /**
+ * Updates the data
+ *
+ * data is a readable stream resource.
+ *
+ * @param resource|string $data
+ * @return string
+ */
+ public function put($data) {
+
+ file_put_contents($this->path,$data);
+ return '"' . md5_file($this->path) . '"';
+
+ }
+
+ /**
+ * Updates the file based on a range specification.
+ *
+ * The first argument is the data, which is either a readable stream
+ * resource or a string.
+ *
+ * The second argument is the type of update we're doing.
+ * This is either:
+ * * 1. append
+ * * 2. update based on a start byte
+ * * 3. update based on an end byte
+ *;
+ * The third argument is the start or end byte.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource|string $data
+ * @param int $rangeType
+ * @param int $offset
+ * @return string|null
+ */
+ public function patch($data, $rangeType, $offset = null) {
+
+ switch($rangeType) {
+ case 1 :
+ $f = fopen($this->path, 'a');
+ break;
+ case 2 :
+ $f = fopen($this->path, 'c');
+ fseek($f,$offset);
+ break;
+ case 3 :
+ $f = fopen($this->path, 'c');
+ fseek($f, $offset, SEEK_END);
+ break;
+ }
+ if (is_string($data)) {
+ fwrite($f, $data);
+ } else {
+ stream_copy_to_stream($data,$f);
+ }
+ fclose($f);
+ return '"' . md5_file($this->path) . '"';
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * @return resource
+ */
+ public function get() {
+
+ return fopen($this->path,'r');
+
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return bool
+ */
+ public function delete() {
+
+ unlink($this->path);
+ return parent::delete();
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string|null
+ */
+ public function getETag() {
+
+ return '"' . md5_file($this->path). '"';
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ public function getContentType() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return filesize($this->path);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php
new file mode 100644
index 0000000..68f4255
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/FSExt/Node.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace OldSabre\DAV\FSExt;
+
+use OldSabre\DAV;
+
+/**
+ * Base node-class
+ *
+ * The node class implements the method used by both the File and the Directory classes
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node extends DAV\FS\Node implements DAV\IProperties {
+
+ /**
+ * Updates properties on this node,
+ *
+ * @param array $properties
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateProperties($properties) {
+
+ $resourceData = $this->getResourceData();
+
+ foreach($properties as $propertyName=>$propertyValue) {
+
+ // If it was null, we need to delete the property
+ if (is_null($propertyValue)) {
+ if (isset($resourceData['properties'][$propertyName])) {
+ unset($resourceData['properties'][$propertyName]);
+ }
+ } else {
+ $resourceData['properties'][$propertyName] = $propertyValue;
+ }
+
+ }
+
+ $this->putResourceData($resourceData);
+ return true;
+ }
+
+ /**
+ * Returns a list of properties for this nodes.;
+ *
+ * The properties list is a list of propertynames the client requested, encoded as xmlnamespace#tagName, for example: http://www.example.org/namespace#author
+ * If the array is empty, all properties should be returned
+ *
+ * @param array $properties
+ * @return array
+ */
+ function getProperties($properties) {
+
+ $resourceData = $this->getResourceData();
+
+ // if the array was empty, we need to return everything
+ if (!$properties) return $resourceData['properties'];
+
+ $props = array();
+ foreach($properties as $property) {
+ if (isset($resourceData['properties'][$property])) $props[$property] = $resourceData['properties'][$property];
+ }
+
+ return $props;
+
+ }
+
+ /**
+ * Returns the path to the resource file
+ *
+ * @return string
+ */
+ protected function getResourceInfoPath() {
+
+ list($parentDir) = DAV\URLUtil::splitPath($this->path);
+ return $parentDir . '/.sabredav';
+
+ }
+
+ /**
+ * Returns all the stored resource information
+ *
+ * @return array
+ */
+ protected function getResourceData() {
+
+ $path = $this->getResourceInfoPath();
+ if (!file_exists($path)) return array('properties' => array());
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'r');
+ flock($handle,LOCK_SH);
+ $data = '';
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // We're all good
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!isset($data[$this->getName()])) {
+ return array('properties' => array());
+ }
+
+ $data = $data[$this->getName()];
+ if (!isset($data['properties'])) $data['properties'] = array();
+ return $data;
+
+ }
+
+ /**
+ * Updates the resource information
+ *
+ * @param array $newData
+ * @return void
+ */
+ protected function putResourceData(array $newData) {
+
+ $path = $this->getResourceInfoPath();
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'a+');
+ flock($handle,LOCK_EX);
+ $data = '';
+
+ rewind($handle);
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ $data[$this->getName()] = $newData;
+ ftruncate($handle,0);
+ rewind($handle);
+
+ fwrite($handle,serialize($data));
+ fclose($handle);
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ list($parentPath, ) = DAV\URLUtil::splitPath($this->path);
+ list(, $newName) = DAV\URLUtil::splitPath($name);
+ $newPath = $parentPath . '/' . $newName;
+
+ // We're deleting the existing resourcedata, and recreating it
+ // for the new path.
+ $resourceData = $this->getResourceData();
+ $this->deleteResourceData();
+
+ rename($this->path,$newPath);
+ $this->path = $newPath;
+ $this->putResourceData($resourceData);
+
+
+ }
+
+ /**
+ * @return bool
+ */
+ public function deleteResourceData() {
+
+ // When we're deleting this node, we also need to delete any resource information
+ $path = $this->getResourceInfoPath();
+ if (!file_exists($path)) return true;
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'a+');
+ flock($handle,LOCK_EX);
+ $data = '';
+
+ rewind($handle);
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (isset($data[$this->getName()])) unset($data[$this->getName()]);
+ ftruncate($handle,0);
+ rewind($handle);
+ fwrite($handle,serialize($data));
+ fclose($handle);
+
+ return true;
+ }
+
+ public function delete() {
+
+ return $this->deleteResourceData();
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php
new file mode 100644
index 0000000..d696eb3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/File.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * File class
+ *
+ * This is a helper class, that should aid in getting file classes setup.
+ * Most of its methods are implemented, and throw permission denied exceptions
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class File extends Node implements IFile {
+
+ /**
+ * Updates the data
+ *
+ * data is a readable stream resource.
+ *
+ * @param resource $data
+ * @return void
+ */
+ public function put($data) {
+
+ throw new Exception\Forbidden('Permission denied to change data');
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ public function get() {
+
+ throw new Exception\Forbidden('Permission denied to read this file');
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return 0;
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return string|null
+ */
+ public function getETag() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ public function getContentType() {
+
+ return null;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php
new file mode 100644
index 0000000..0468ee1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ICollection.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The ICollection Interface
+ *
+ * This interface should be implemented by each class that represents a collection
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICollection extends INode {
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After successful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ function createFile($name, $data = null);
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @return void
+ */
+ function createDirectory($name);
+
+ /**
+ * Returns a specific child node, referenced by its name
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @return DAV\INode
+ */
+ function getChild($name);
+
+ /**
+ * Returns an array with all the child nodes
+ *
+ * @return DAV\INode[]
+ */
+ function getChildren();
+
+ /**
+ * Checks if a child-node with the specified name exists
+ *
+ * @param string $name
+ * @return bool
+ */
+ function childExists($name);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php
new file mode 100644
index 0000000..7894f58
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IExtendedCollection.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The IExtendedCollection interface.
+ *
+ * This interface can be used to create special-type of collection-resources
+ * as defined by RFC 5689.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IExtendedCollection extends ICollection {
+
+ /**
+ * Creates a new collection
+ *
+ * @param string $name
+ * @param array $resourceType
+ * @param array $properties
+ * @return void
+ */
+ function createExtendedCollection($name, array $resourceType, array $properties);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php
new file mode 100644
index 0000000..476ccbd
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IFile.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * This interface represents a file in the directory tree
+ *
+ * A file is a bit of a broad definition. In general it implies that on
+ * this specific node a PUT or GET method may be performed, to either update,
+ * or retrieve the contents of the file.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IFile extends INode {
+
+ /**
+ * Updates the data
+ *
+ * The data argument is a readable stream resource.
+ *
+ * After a succesful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * If you don't plan to store the file byte-by-byte, and you return a
+ * different object on a subsequent GET you are strongly recommended to not
+ * return an ETag, and just return null.
+ *
+ * @param resource $data
+ * @return string|null
+ */
+ function put($data);
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ function get();
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return string|null
+ */
+ function getContentType();
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return void
+ */
+ function getETag();
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ function getSize();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php
new file mode 100644
index 0000000..09c254f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/INode.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The INode interface is the base interface, and the parent class of both ICollection and IFile
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INode {
+
+ /**
+ * Deleted the current node
+ *
+ * @return void
+ */
+ function delete();
+
+ /**
+ * Returns the name of the node.
+ *
+ * This is used to generate the url.
+ *
+ * @return string
+ */
+ function getName();
+
+ /**
+ * Renames the node
+ *
+ * @param string $name The new name
+ * @return void
+ */
+ function setName($name);
+
+ /**
+ * Returns the last modification time, as a unix timestamp
+ *
+ * @return int
+ */
+ function getLastModified();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php
new file mode 100644
index 0000000..8f97ebc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IProperties.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * IProperties interface
+ *
+ * Implement this interface to support custom WebDAV properties requested and sent from clients.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProperties extends INode {
+
+ /**
+ * Updates properties on this node,
+ *
+ * The properties array uses the propertyName in clark-notation as key,
+ * and the array value for the property value. In the case a property
+ * should be deleted, the property value will be null.
+ *
+ * This method must be atomic. If one property cannot be changed, the
+ * entire operation must fail.
+ *
+ * If the operation was successful, true can be returned.
+ * If the operation failed, false can be returned.
+ *
+ * Deletion of a non-existent property is always successful.
+ *
+ * Lastly, it is optional to return detailed information about any
+ * failures. In this case an array should be returned with the following
+ * structure:
+ *
+ * array(
+ * 403 => array(
+ * '{DAV:}displayname' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}owner' => null,
+ * )
+ * )
+ *
+ * In this example it was forbidden to update {DAV:}displayname.
+ * (403 Forbidden), which in turn also caused {DAV:}owner to fail
+ * (424 Failed Dependency) because the request needs to be atomic.
+ *
+ * @param array $mutations
+ * @return bool|array
+ */
+ function updateProperties($mutations);
+
+ /**
+ * Returns a list of properties for this nodes.
+ *
+ * The properties list is a list of propertynames the client requested,
+ * encoded in clark-notation {xmlnamespace}tagname
+ *
+ * If the array is empty, it means 'all properties' were requested.
+ *
+ * Note that it's fine to liberally give properties back, instead of
+ * conforming to the list of requested properties.
+ * The Server class will filter out the extra.
+ *
+ * @param array $properties
+ * @return void
+ */
+ function getProperties($properties);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php
new file mode 100644
index 0000000..074516e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/IQuota.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * IQuota interface
+ *
+ * Implement this interface to add the ability to return quota information. The ObjectTree
+ * will check for quota information on any given node. If the information is not available it will
+ * attempt to fetch the information from the root node.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IQuota extends ICollection {
+
+ /**
+ * Returns the quota information
+ *
+ * This method MUST return an array with 2 values, the first being the total used space,
+ * the second the available space (in bytes)
+ */
+ function getQuotaInfo();
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php
new file mode 100644
index 0000000..c89d37c
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/AbstractBackend.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks;
+
+/**
+ * This is an Abstract clas for lock backends.
+ *
+ * Currently this backend has no function, but it exists for consistency, and
+ * to ensure that if default code is required in the backend, there will be a
+ * non-bc-breaking way to do so.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php
new file mode 100644
index 0000000..1a768b0
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/BackendInterface.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks;
+
+/**
+ * If you are defining your own Locks backend, you must implement this
+ * interface.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks);
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param Locks\LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri,Locks\LockInfo $lockInfo);
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param Locks\LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri,Locks\LockInfo $lockInfo);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php
new file mode 100644
index 0000000..3a61f40
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/FS.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks\LockInfo;
+
+/**
+ * This Lock Backend stores all its data in the filesystem in separate file per
+ * node.
+ *
+ * This Lock Manager is now deprecated. It has a bug that allows parent
+ * collections to be deletes when children deeper in the tree are locked.
+ *
+ * This also means that using this backend means you will not pass the Neon
+ * Litmus test.
+ *
+ * You are recommended to use either the PDO or the File backend instead.
+ *
+ * @deprecated
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class FS extends AbstractBackend {
+
+ /**
+ * The default data directory
+ *
+ * @var string
+ */
+ private $dataDir;
+
+ public function __construct($dataDir) {
+
+ $this->dataDir = $dataDir;
+
+ }
+
+ protected function getFileNameForUri($uri) {
+
+ return $this->dataDir . '/sabredav_' . md5($uri) . '.locks';
+
+ }
+
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks) {
+
+ $lockList = array();
+ $currentPath = '';
+
+ foreach(explode('/',$uri) as $uriPart) {
+
+ // weird algorithm that can probably be improved, but we're traversing the path top down
+ if ($currentPath) $currentPath.='/';
+ $currentPath.=$uriPart;
+
+ $uriLocks = $this->getData($currentPath);
+
+ foreach($uriLocks as $uriLock) {
+
+ // Unless we're on the leaf of the uri-tree we should ignore locks with depth 0
+ if($uri==$currentPath || $uriLock->depth!=0) {
+ $uriLock->uri = $currentPath;
+ $lockList[] = $uriLock;
+ }
+
+ }
+
+ }
+
+ // Checking if we can remove any of these locks
+ foreach($lockList as $k=>$lock) {
+ if (time() > $lock->timeout + $lock->created) unset($lockList[$k]);
+ }
+ return $lockList;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 1800;
+ $lockInfo->created = time();
+
+ $locks = $this->getLocks($uri,false);
+ foreach($locks as $k=>$lock) {
+ if ($lock->token == $lockInfo->token) unset($locks[$k]);
+ }
+ $locks[] = $lockInfo;
+ $this->putData($uri,$locks);
+ return true;
+
+ }
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo) {
+
+ $locks = $this->getLocks($uri,false);
+ foreach($locks as $k=>$lock) {
+
+ if ($lock->token == $lockInfo->token) {
+
+ unset($locks[$k]);
+ $this->putData($uri,$locks);
+ return true;
+
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns the stored data for a uri
+ *
+ * @param string $uri
+ * @return array
+ */
+ protected function getData($uri) {
+
+ $path = $this->getFilenameForUri($uri);
+ if (!file_exists($path)) return array();
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'r');
+ flock($handle,LOCK_SH);
+ $data = '';
+
+ // Reading data until the eof
+ while(!feof($handle)) {
+ $data.=fread($handle,8192);
+ }
+
+ // We're all good
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!$data) return array();
+ return $data;
+
+ }
+
+ /**
+ * Updates the lock information
+ *
+ * @param string $uri
+ * @param array $newData
+ * @return void
+ */
+ protected function putData($uri,array $newData) {
+
+ $path = $this->getFileNameForUri($uri);
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($path,'a+');
+ flock($handle,LOCK_EX);
+ ftruncate($handle,0);
+ rewind($handle);
+
+ fwrite($handle,serialize($newData));
+ fclose($handle);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php
new file mode 100644
index 0000000..b4eb58b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/File.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks\LockInfo;
+
+/**
+ * The Lock manager allows you to handle all file-locks centrally.
+ *
+ * This Lock Manager stores all its data in a single file.
+ *
+ * Note that this is not nearly as robust as a database, you are encouraged
+ * to use the PDO backend instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class File extends AbstractBackend {
+
+ /**
+ * The storage file
+ *
+ * @var string
+ */
+ private $locksFile;
+
+ /**
+ * Constructor
+ *
+ * @param string $locksFile path to file
+ */
+ public function __construct($locksFile) {
+
+ $this->locksFile = $locksFile;
+
+ }
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks) {
+
+ $newLocks = array();
+
+ $locks = $this->getData();
+
+ foreach($locks as $lock) {
+
+ if ($lock->uri === $uri ||
+ //deep locks on parents
+ ($lock->depth!=0 && strpos($uri, $lock->uri . '/')===0) ||
+
+ // locks on children
+ ($returnChildLocks && (strpos($lock->uri, $uri . '/')===0)) ) {
+
+ $newLocks[] = $lock;
+
+ }
+
+ }
+
+ // Checking if we can remove any of these locks
+ foreach($newLocks as $k=>$lock) {
+ if (time() > $lock->timeout + $lock->created) unset($newLocks[$k]);
+ }
+ return $newLocks;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 1800;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getData();
+
+ foreach($locks as $k=>$lock) {
+ if (
+ ($lock->token == $lockInfo->token) ||
+ (time() > $lock->timeout + $lock->created)
+ ) {
+ unset($locks[$k]);
+ }
+ }
+ $locks[] = $lockInfo;
+ $this->putData($locks);
+ return true;
+
+ }
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo) {
+
+ $locks = $this->getData();
+ foreach($locks as $k=>$lock) {
+
+ if ($lock->token == $lockInfo->token) {
+
+ unset($locks[$k]);
+ $this->putData($locks);
+ return true;
+
+ }
+ }
+ return false;
+
+ }
+
+ /**
+ * Loads the lockdata from the filesystem.
+ *
+ * @return array
+ */
+ protected function getData() {
+
+ if (!file_exists($this->locksFile)) return array();
+
+ // opening up the file, and creating a shared lock
+ $handle = fopen($this->locksFile,'r');
+ flock($handle,LOCK_SH);
+
+ // Reading data until the eof
+ $data = stream_get_contents($handle);
+
+ // We're all good
+ fclose($handle);
+
+ // Unserializing and checking if the resource file contains data for this file
+ $data = unserialize($data);
+ if (!$data) return array();
+ return $data;
+
+ }
+
+ /**
+ * Saves the lockdata
+ *
+ * @param array $newData
+ * @return void
+ */
+ protected function putData(array $newData) {
+
+ // opening up the file, and creating an exclusive lock
+ $handle = fopen($this->locksFile,'a+');
+ flock($handle,LOCK_EX);
+
+ // We can only truncate and rewind once the lock is acquired.
+ ftruncate($handle,0);
+ rewind($handle);
+
+ fwrite($handle,serialize($newData));
+ fclose($handle);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php
new file mode 100644
index 0000000..520a84b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Backend/PDO.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace OldSabre\DAV\Locks\Backend;
+
+use OldSabre\DAV\Locks\LockInfo;
+
+/**
+ * The Lock manager allows you to handle all file-locks centrally.
+ *
+ * This Lock Manager stores all its data in a database. You must pass a PDO
+ * connection object in the constructor.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * The PDO connection object
+ *
+ * @var pdo
+ */
+ private $pdo;
+
+ /**
+ * The PDO tablename this backend uses.
+ *
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * Constructor
+ *
+ * @param PDO $pdo
+ * @param string $tableName
+ */
+ public function __construct(\PDO $pdo, $tableName = 'locks') {
+
+ $this->pdo = $pdo;
+ $this->tableName = $tableName;
+
+ }
+
+ /**
+ * Returns a list of OldSabre\DAV\Locks\LockInfo objects
+ *
+ * This method should return all the locks for a particular uri, including
+ * locks that might be set on a parent uri.
+ *
+ * If returnChildLocks is set to true, this method should also look for
+ * any locks in the subtree of the uri for locks.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks) {
+
+ // NOTE: the following 10 lines or so could be easily replaced by
+ // pure sql. MySQL's non-standard string concatenation prevents us
+ // from doing this though.
+ $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM '.$this->tableName.' WHERE ((created + timeout) > CAST(? AS UNSIGNED INTEGER)) AND ((uri = ?)';
+ $params = array(time(),$uri);
+
+ // We need to check locks for every part in the uri.
+ $uriParts = explode('/',$uri);
+
+ // We already covered the last part of the uri
+ array_pop($uriParts);
+
+ $currentPath='';
+
+ foreach($uriParts as $part) {
+
+ if ($currentPath) $currentPath.='/';
+ $currentPath.=$part;
+
+ $query.=' OR (depth!=0 AND uri = ?)';
+ $params[] = $currentPath;
+
+ }
+
+ if ($returnChildLocks) {
+
+ $query.=' OR (uri LIKE ?)';
+ $params[] = $uri . '/%';
+
+ }
+ $query.=')';
+
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($params);
+ $result = $stmt->fetchAll();
+
+ $lockList = array();
+ foreach($result as $row) {
+
+ $lockInfo = new LockInfo();
+ $lockInfo->owner = $row['owner'];
+ $lockInfo->token = $row['token'];
+ $lockInfo->timeout = $row['timeout'];
+ $lockInfo->created = $row['created'];
+ $lockInfo->scope = $row['scope'];
+ $lockInfo->depth = $row['depth'];
+ $lockInfo->uri = $row['uri'];
+ $lockList[] = $lockInfo;
+
+ }
+
+ return $lockList;
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lock($uri, LockInfo $lockInfo) {
+
+ // We're making the lock timeout 30 minutes
+ $lockInfo->timeout = 30*60;
+ $lockInfo->created = time();
+ $lockInfo->uri = $uri;
+
+ $locks = $this->getLocks($uri,false);
+ $exists = false;
+ foreach($locks as $lock) {
+ if ($lock->token == $lockInfo->token) $exists = true;
+ }
+
+ if ($exists) {
+ $stmt = $this->pdo->prepare('UPDATE '.$this->tableName.' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?');
+ $stmt->execute(array($lockInfo->owner,$lockInfo->timeout,$lockInfo->scope,$lockInfo->depth,$uri,$lockInfo->created,$lockInfo->token));
+ } else {
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)');
+ $stmt->execute(array($lockInfo->owner,$lockInfo->timeout,$lockInfo->scope,$lockInfo->depth,$uri,$lockInfo->created,$lockInfo->token));
+ }
+
+ return true;
+
+ }
+
+
+
+ /**
+ * Removes a lock from a uri
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlock($uri, LockInfo $lockInfo) {
+
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE uri = ? AND token = ?');
+ $stmt->execute(array($uri,$lockInfo->token));
+
+ return $stmt->rowCount()===1;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php
new file mode 100644
index 0000000..77bba97
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/LockInfo.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace OldSabre\DAV\Locks;
+
+/**
+ * LockInfo class
+ *
+ * An object of the LockInfo class holds all the information relevant to a
+ * single lock.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LockInfo {
+
+ /**
+ * A shared lock
+ */
+ const SHARED = 1;
+
+ /**
+ * An exclusive lock
+ */
+ const EXCLUSIVE = 2;
+
+ /**
+ * A never expiring timeout
+ */
+ const TIMEOUT_INFINITE = -1;
+
+ /**
+ * The owner of the lock
+ *
+ * @var string
+ */
+ public $owner;
+
+ /**
+ * The locktoken
+ *
+ * @var string
+ */
+ public $token;
+
+ /**
+ * How long till the lock is expiring
+ *
+ * @var int
+ */
+ public $timeout;
+
+ /**
+ * UNIX Timestamp of when this lock was created
+ *
+ * @var int
+ */
+ public $created;
+
+ /**
+ * Exclusive or shared lock
+ *
+ * @var int
+ */
+ public $scope = self::EXCLUSIVE;
+
+ /**
+ * Depth of lock, can be 0 or OldSabre\DAV\Server::DEPTH_INFINITY
+ */
+ public $depth = 0;
+
+ /**
+ * The uri this lock locks
+ *
+ * TODO: This value is not always set
+ * @var mixed
+ */
+ public $uri;
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php
new file mode 100644
index 0000000..df78b92
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Locks/Plugin.php
@@ -0,0 +1,649 @@
+<?php
+
+namespace OldSabre\DAV\Locks;
+
+use OldSabre\DAV;
+
+/**
+ * Locking plugin
+ *
+ * This plugin provides locking support to a WebDAV server.
+ * The easiest way to get started, is by hooking it up as such:
+ *
+ * $lockBackend = new OldSabre\DAV\Locks\Backend\File('./mylockdb');
+ * $lockPlugin = new OldSabre\DAV\Locks\Plugin($lockBackend);
+ * $server->addPlugin($lockPlugin);
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * locksBackend
+ *
+ * @var Backend\Backend\Interface
+ */
+ protected $locksBackend;
+
+ /**
+ * server
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * __construct
+ *
+ * @param Backend\BackendInterface $locksBackend
+ */
+ public function __construct(Backend\BackendInterface $locksBackend = null) {
+
+ $this->locksBackend = $locksBackend;
+
+ }
+
+ /**
+ * Initializes the plugin
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
+ $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'),50);
+ $server->subscribeEvent('afterGetProperties',array($this,'afterGetProperties'));
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'locks';
+
+ }
+
+ /**
+ * This method is called by the Server if the user used an HTTP method
+ * the server didn't recognize.
+ *
+ * This plugin intercepts the LOCK and UNLOCK methods.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ switch($method) {
+
+ case 'LOCK' : $this->httpLock($uri); return false;
+ case 'UNLOCK' : $this->httpUnlock($uri); return false;
+
+ }
+
+ }
+
+ /**
+ * This method is called after most properties have been found
+ * it allows us to add in any Lock-related properties
+ *
+ * @param string $path
+ * @param array $newProperties
+ * @return bool
+ */
+ public function afterGetProperties($path, &$newProperties) {
+
+ foreach($newProperties[404] as $propName=>$discard) {
+
+ switch($propName) {
+
+ case '{DAV:}supportedlock' :
+ $val = false;
+ if ($this->locksBackend) $val = true;
+ $newProperties[200][$propName] = new DAV\Property\SupportedLock($val);
+ unset($newProperties[404][$propName]);
+ break;
+
+ case '{DAV:}lockdiscovery' :
+ $newProperties[200][$propName] = new DAV\Property\LockDiscovery($this->getLocks($path));
+ unset($newProperties[404][$propName]);
+ break;
+
+ }
+
+
+ }
+ return true;
+
+ }
+
+
+ /**
+ * This method is called before the logic for any HTTP method is
+ * handled.
+ *
+ * This plugin uses that feature to intercept access to locked resources.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ switch($method) {
+
+ case 'DELETE' :
+ $lastLock = null;
+ if (!$this->validateLock($uri,$lastLock, true))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ case 'MKCOL' :
+ case 'PROPPATCH' :
+ case 'PUT' :
+ case 'PATCH' :
+ $lastLock = null;
+ if (!$this->validateLock($uri,$lastLock))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ case 'MOVE' :
+ $lastLock = null;
+ if (!$this->validateLock(array(
+ $uri,
+ $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
+ ),$lastLock, true))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ case 'COPY' :
+ $lastLock = null;
+ if (!$this->validateLock(
+ $this->server->calculateUri($this->server->httpRequest->getHeader('Destination')),
+ $lastLock, true))
+ throw new DAV\Exception\Locked($lastLock);
+ break;
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ if ($this->locksBackend)
+ return array('LOCK','UNLOCK');
+
+ return array();
+
+ }
+
+ /**
+ * Returns a list of features for the HTTP OPTIONS Dav: header.
+ *
+ * In this case this is only the number 2. The 2 in the Dav: header
+ * indicates the server supports locks.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array(2);
+
+ }
+
+ /**
+ * Returns all lock information on a particular uri
+ *
+ * This function should return an array with OldSabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array.
+ *
+ * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree
+ * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object
+ * for any possible locks and return those as well.
+ *
+ * @param string $uri
+ * @param bool $returnChildLocks
+ * @return array
+ */
+ public function getLocks($uri, $returnChildLocks = false) {
+
+ $lockList = array();
+
+ if ($this->locksBackend)
+ $lockList = array_merge($lockList,$this->locksBackend->getLocks($uri, $returnChildLocks));
+
+ return $lockList;
+
+ }
+
+ /**
+ * Locks an uri
+ *
+ * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock
+ * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type
+ * of lock (shared or exclusive) and the owner of the lock
+ *
+ * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock
+ *
+ * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpLock($uri) {
+
+ $lastLock = null;
+ if (!$this->validateLock($uri,$lastLock)) {
+
+ // If the existing lock was an exclusive lock, we need to fail
+ if (!$lastLock || $lastLock->scope == LockInfo::EXCLUSIVE) {
+ //var_dump($lastLock);
+ throw new DAV\Exception\ConflictingLock($lastLock);
+ }
+
+ }
+
+ if ($body = $this->server->httpRequest->getBody(true)) {
+ // This is a new lock request
+ $lockInfo = $this->parseLockRequest($body);
+ $lockInfo->depth = $this->server->getHTTPDepth();
+ $lockInfo->uri = $uri;
+ if($lastLock && $lockInfo->scope != LockInfo::SHARED) throw new DAV\Exception\ConflictingLock($lastLock);
+
+ } elseif ($lastLock) {
+
+ // This must have been a lock refresh
+ $lockInfo = $lastLock;
+
+ // The resource could have been locked through another uri.
+ if ($uri!=$lockInfo->uri) $uri = $lockInfo->uri;
+
+ } else {
+
+ // There was neither a lock refresh nor a new lock request
+ throw new DAV\Exception\BadRequest('An xml body is required for lock requests');
+
+ }
+
+ if ($timeout = $this->getTimeoutHeader()) $lockInfo->timeout = $timeout;
+
+ $newFile = false;
+
+ // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
+ try {
+ $this->server->tree->getNodeForPath($uri);
+
+ // We need to call the beforeWriteContent event for RFC3744
+ // Edit: looks like this is not used, and causing problems now.
+ //
+ // See Issue 222
+ // $this->server->broadcastEvent('beforeWriteContent',array($uri));
+
+ } catch (DAV\Exception\NotFound $e) {
+
+ // It didn't, lets create it
+ $this->server->createFile($uri,fopen('php://memory','r'));
+ $newFile = true;
+
+ }
+
+ $this->lockNode($uri,$lockInfo);
+
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Lock-Token','<opaquelocktoken:' . $lockInfo->token . '>');
+ $this->server->httpResponse->sendStatus($newFile?201:200);
+ $this->server->httpResponse->sendBody($this->generateLockResponse($lockInfo));
+
+ }
+
+ /**
+ * Unlocks a uri
+ *
+ * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header
+ * The server should return 204 (No content) on success
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpUnlock($uri) {
+
+ $lockToken = $this->server->httpRequest->getHeader('Lock-Token');
+
+ // If the locktoken header is not supplied, we need to throw a bad request exception
+ if (!$lockToken) throw new DAV\Exception\BadRequest('No lock token was supplied');
+
+ $locks = $this->getLocks($uri);
+
+ // Windows sometimes forgets to include < and > in the Lock-Token
+ // header
+ if ($lockToken[0]!=='<') $lockToken = '<' . $lockToken . '>';
+
+ foreach($locks as $lock) {
+
+ if ('<opaquelocktoken:' . $lock->token . '>' == $lockToken) {
+
+ $this->unlockNode($uri,$lock);
+ $this->server->httpResponse->setHeader('Content-Length','0');
+ $this->server->httpResponse->sendStatus(204);
+ return;
+
+ }
+
+ }
+
+ // If we got here, it means the locktoken was invalid
+ throw new DAV\Exception\LockTokenMatchesRequestUri();
+
+ }
+
+ /**
+ * Locks a uri
+ *
+ * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored
+ * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function lockNode($uri,LockInfo $lockInfo) {
+
+ if (!$this->server->broadcastEvent('beforeLock',array($uri,$lockInfo))) return;
+
+ if ($this->locksBackend) return $this->locksBackend->lock($uri,$lockInfo);
+ throw new DAV\Exception\MethodNotAllowed('Locking support is not enabled for this resource. No Locking backend was found so if you didn\'t expect this error, please check your configuration.');
+
+ }
+
+ /**
+ * Unlocks a uri
+ *
+ * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified
+ *
+ * @param string $uri
+ * @param LockInfo $lockInfo
+ * @return bool
+ */
+ public function unlockNode($uri, LockInfo $lockInfo) {
+
+ if (!$this->server->broadcastEvent('beforeUnlock',array($uri,$lockInfo))) return;
+ if ($this->locksBackend) return $this->locksBackend->unlock($uri,$lockInfo);
+
+ }
+
+
+ /**
+ * Returns the contents of the HTTP Timeout header.
+ *
+ * The method formats the header into an integer.
+ *
+ * @return int
+ */
+ public function getTimeoutHeader() {
+
+ $header = $this->server->httpRequest->getHeader('Timeout');
+
+ if ($header) {
+
+ if (stripos($header,'second-')===0) $header = (int)(substr($header,7));
+ else if (strtolower($header)=='infinite') $header = LockInfo::TIMEOUT_INFINITE;
+ else throw new DAV\Exception\BadRequest('Invalid HTTP timeout header');
+
+ } else {
+
+ $header = 0;
+
+ }
+
+ return $header;
+
+ }
+
+ /**
+ * Generates the response for successful LOCK requests
+ *
+ * @param LockInfo $lockInfo
+ * @return string
+ */
+ protected function generateLockResponse(LockInfo $lockInfo) {
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+
+ $prop = $dom->createElementNS('DAV:','d:prop');
+ $dom->appendChild($prop);
+
+ $lockDiscovery = $dom->createElementNS('DAV:','d:lockdiscovery');
+ $prop->appendChild($lockDiscovery);
+
+ $lockObj = new DAV\Property\LockDiscovery(array($lockInfo),true);
+ $lockObj->serialize($this->server,$lockDiscovery);
+
+ return $dom->saveXML();
+
+ }
+
+ /**
+ * validateLock should be called when a write operation is about to happen
+ * It will check if the requested url is locked, and see if the correct lock tokens are passed
+ *
+ * @param mixed $urls List of relevant urls. Can be an array, a string or nothing at all for the current request uri
+ * @param mixed $lastLock This variable will be populated with the last checked lock object (OldSabre\DAV\Locks\LockInfo)
+ * @param bool $checkChildLocks If set to true, this function will also look for any locks set on child resources of the supplied urls. This is needed for for example deletion of entire trees.
+ * @return bool
+ */
+ protected function validateLock($urls = null,&$lastLock = null, $checkChildLocks = false) {
+
+ if (is_null($urls)) {
+ $urls = array($this->server->getRequestUri());
+ } elseif (is_string($urls)) {
+ $urls = array($urls);
+ } elseif (!is_array($urls)) {
+ throw new DAV\Exception('The urls parameter should either be null, a string or an array');
+ }
+
+ $conditions = $this->getIfConditions();
+
+ // We're going to loop through the urls and make sure all lock conditions are satisfied
+ foreach($urls as $url) {
+
+ $locks = $this->getLocks($url, $checkChildLocks);
+
+ // If there were no conditions, but there were locks, we fail
+ if (!$conditions && $locks) {
+ reset($locks);
+ $lastLock = current($locks);
+ return false;
+ }
+
+ // If there were no locks or conditions, we go to the next url
+ if (!$locks && !$conditions) continue;
+
+ foreach($conditions as $condition) {
+
+ if (!$condition['uri']) {
+ $conditionUri = $this->server->getRequestUri();
+ } else {
+ $conditionUri = $this->server->calculateUri($condition['uri']);
+ }
+
+ // If the condition has a url, and it isn't part of the affected url at all, check the next condition
+ if ($conditionUri && strpos($url,$conditionUri)!==0) continue;
+
+ // The tokens array contians arrays with 2 elements. 0=true/false for normal/not condition, 1=locktoken
+ // At least 1 condition has to be satisfied
+ foreach($condition['tokens'] as $conditionToken) {
+
+ $etagValid = true;
+ $lockValid = true;
+
+ // key 2 can contain an etag
+ if ($conditionToken[2]) {
+
+ $uri = $conditionUri?$conditionUri:$this->server->getRequestUri();
+ $node = $this->server->tree->getNodeForPath($uri);
+ $etagValid = $node instanceof DAV\IFile && $node->getETag()==$conditionToken[2];
+
+ }
+
+ // key 1 can contain a lock token
+ if ($conditionToken[1]) {
+
+ $lockValid = false;
+ // Match all the locks
+ foreach($locks as $lockIndex=>$lock) {
+
+ $lockToken = 'opaquelocktoken:' . $lock->token;
+
+ // Checking NOT
+ if (!$conditionToken[0] && $lockToken != $conditionToken[1]) {
+
+ // Condition valid, onto the next
+ $lockValid = true;
+ break;
+ }
+ if ($conditionToken[0] && $lockToken == $conditionToken[1]) {
+
+ $lastLock = $lock;
+ // Condition valid and lock matched
+ unset($locks[$lockIndex]);
+ $lockValid = true;
+ break;
+
+ }
+
+ }
+
+ }
+
+ // If, after checking both etags and locks they are stil valid,
+ // we can continue with the next condition.
+ if ($etagValid && $lockValid) continue 2;
+ }
+ // No conditions matched, so we fail
+ throw new DAV\Exception\PreconditionFailed('The tokens provided in the if header did not match','If');
+ }
+
+ // Conditions were met, we'll also need to check if all the locks are gone
+ if (count($locks)) {
+
+ reset($locks);
+
+ // There's still locks, we fail
+ $lastLock = current($locks);
+ return false;
+
+ }
+
+
+ }
+
+ // We got here, this means every condition was satisfied
+ return true;
+
+ }
+
+ /**
+ * This method is created to extract information from the WebDAV HTTP 'If:' header
+ *
+ * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
+ * The function will return an array, containing structs with the following keys
+ *
+ * * uri - the uri the condition applies to. If this is returned as an
+ * empty string, this implies it's referring to the request url.
+ * * tokens - The lock token. another 2 dimensional array containing 2 elements (0 = true/false.. If this is a negative condition its set to false, 1 = the actual token)
+ * * etag - an etag, if supplied
+ *
+ * @return array
+ */
+ public function getIfConditions() {
+
+ $header = $this->server->httpRequest->getHeader('If');
+ if (!$header) return array();
+
+ $matches = array();
+
+ $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
+ preg_match_all($regex,$header,$matches,PREG_SET_ORDER);
+
+ $conditions = array();
+
+ foreach($matches as $match) {
+
+ $condition = array(
+ 'uri' => $match['uri'],
+ 'tokens' => array(
+ array($match['not']?0:1,$match['token'],isset($match['etag'])?$match['etag']:'')
+ ),
+ );
+
+ if (!$condition['uri'] && count($conditions)) $conditions[count($conditions)-1]['tokens'][] = array(
+ $match['not']?0:1,
+ $match['token'],
+ isset($match['etag'])?$match['etag']:''
+ );
+ else {
+ $conditions[] = $condition;
+ }
+
+ }
+
+ return $conditions;
+
+ }
+
+ /**
+ * Parses a webdav lock xml body, and returns a new OldSabre\DAV\Locks\LockInfo object
+ *
+ * @param string $body
+ * @return DAV\Locks\LockInfo
+ */
+ protected function parseLockRequest($body) {
+
+ // Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
+ // 5.4.13.
+ $previous = libxml_disable_entity_loader(true);
+
+
+ $xml = simplexml_load_string(
+ DAV\XMLUtil::convertDAVNamespace($body),
+ null,
+ LIBXML_NOWARNING);
+ libxml_disable_entity_loader($previous);
+
+ $xml->registerXPathNamespace('d','urn:DAV');
+ $lockInfo = new LockInfo();
+
+ $children = $xml->children("urn:DAV");
+ $lockInfo->owner = (string)$children->owner;
+
+ $lockInfo->token = DAV\UUIDUtil::getUUID();
+ $lockInfo->scope = count($xml->xpath('d:lockscope/d:exclusive'))>0 ? LockInfo::EXCLUSIVE : LockInfo::SHARED;
+
+ return $lockInfo;
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php
new file mode 100644
index 0000000..cfd159b
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Mount/Plugin.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace OldSabre\DAV\Mount;
+
+use OldSabre\DAV;
+
+/**
+ * This plugin provides support for RFC4709: Mounting WebDAV servers
+ *
+ * Simply append ?mount to any collection to generate the davmount response.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Reference to Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin and registers event handles
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $this->server->subscribeEvent('beforeMethod',array($this,'beforeMethod'), 90);
+
+ }
+
+ /**
+ * 'beforeMethod' event handles. This event handles intercepts GET requests ending
+ * with ?mount
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ if ($method!='GET') return;
+ if ($this->server->httpRequest->getQueryString()!='mount') return;
+
+ $currentUri = $this->server->httpRequest->getAbsoluteUri();
+
+ // Stripping off everything after the ?
+ list($currentUri) = explode('?',$currentUri);
+
+ $this->davMount($currentUri);
+
+ // Returning false to break the event chain
+ return false;
+
+ }
+
+ /**
+ * Generates the davmount response
+ *
+ * @param string $uri absolute uri
+ * @return void
+ */
+ public function davMount($uri) {
+
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->setHeader('Content-Type','application/davmount+xml');
+ ob_start();
+ echo '<?xml version="1.0"?>', "\n";
+ echo "<dm:mount xmlns:dm=\"http://purl.org/NET/webdav/mount\">\n";
+ echo " <dm:url>", htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "</dm:url>\n";
+ echo "</dm:mount>";
+ $this->server->httpResponse->sendBody(ob_get_clean());
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php
new file mode 100644
index 0000000..77d7229
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Node.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Node class
+ *
+ * This is a helper class, that should aid in getting nodes setup.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Node implements INode {
+
+ /**
+ * Returns the last modification time
+ *
+ * In this case, it will simply return the current time
+ *
+ * @return int
+ */
+ public function getLastModified() {
+
+ return time();
+
+ }
+
+ /**
+ * Deletes the current node
+ *
+ * @throws OldSabre\DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete() {
+
+ throw new Exception\Forbidden('Permission denied to delete node');
+
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws OldSabre\DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name) {
+
+ throw new Exception\Forbidden('Permission denied to rename file');
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php
new file mode 100644
index 0000000..57057d9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ObjectTree.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * ObjectTree class
+ *
+ * This implementation of the Tree class makes use of the INode, IFile and ICollection API's
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ObjectTree extends Tree {
+
+ /**
+ * The root node
+ *
+ * @var ICollection
+ */
+ protected $rootNode;
+
+ /**
+ * This is the node cache. Accessed nodes are stored here
+ *
+ * @var array
+ */
+ protected $cache = array();
+
+ /**
+ * Creates the object
+ *
+ * This method expects the rootObject to be passed as a parameter
+ *
+ * @param ICollection $rootNode
+ */
+ public function __construct(ICollection $rootNode) {
+
+ $this->rootNode = $rootNode;
+
+ }
+
+ /**
+ * Returns the INode object for the requested path
+ *
+ * @param string $path
+ * @return INode
+ */
+ public function getNodeForPath($path) {
+
+ $path = trim($path,'/');
+ if (isset($this->cache[$path])) return $this->cache[$path];
+
+ // Is it the root node?
+ if (!strlen($path)) {
+ return $this->rootNode;
+ }
+
+ // Attempting to fetch its parent
+ list($parentName, $baseName) = URLUtil::splitPath($path);
+
+ // If there was no parent, we must simply ask it from the root node.
+ if ($parentName==="") {
+ $node = $this->rootNode->getChild($baseName);
+ } else {
+ // Otherwise, we recursively grab the parent and ask him/her.
+ $parent = $this->getNodeForPath($parentName);
+
+ if (!($parent instanceof ICollection))
+ throw new Exception\NotFound('Could not find node at path: ' . $path);
+
+ $node = $parent->getChild($baseName);
+
+ }
+
+ $this->cache[$path] = $node;
+ return $node;
+
+ }
+
+ /**
+ * This function allows you to check if a node exists.
+ *
+ * @param string $path
+ * @return bool
+ */
+ public function nodeExists($path) {
+
+ try {
+
+ // The root always exists
+ if ($path==='') return true;
+
+ list($parent, $base) = URLUtil::splitPath($path);
+
+ $parentNode = $this->getNodeForPath($parent);
+ if (!$parentNode instanceof ICollection) return false;
+ return $parentNode->childExists($base);
+
+ } catch (Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Returns a list of childnodes for a given path.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getChildren($path) {
+
+ $node = $this->getNodeForPath($path);
+ $children = $node->getChildren();
+ foreach($children as $child) {
+
+ $this->cache[trim($path,'/') . '/' . $child->getName()] = $child;
+
+ }
+ return $children;
+
+ }
+
+ /**
+ * This method is called with every tree update
+ *
+ * Examples of tree updates are:
+ * * node deletions
+ * * node creations
+ * * copy
+ * * move
+ * * renaming nodes
+ *
+ * If Tree classes implement a form of caching, this will allow
+ * them to make sure caches will be expired.
+ *
+ * If a path is passed, it is assumed that the entire subtree is dirty
+ *
+ * @param string $path
+ * @return void
+ */
+ public function markDirty($path) {
+
+ // We don't care enough about sub-paths
+ // flushing the entire cache
+ $path = trim($path,'/');
+ foreach($this->cache as $nodePath=>$node) {
+ if ($nodePath == $path || strpos($nodePath,$path.'/')===0)
+ unset($this->cache[$nodePath]);
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php
new file mode 100644
index 0000000..2fbdf1e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IFile.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace OldSabre\DAV\PartialUpdate;
+
+use OldSabre\DAV;
+
+/**
+ * This interface is deprecated. Use IPatchSupport instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ * @deprecated
+ */
+interface IFile extends DAV\IFile {
+
+ /**
+ * Updates the data at a given offset
+ *
+ * The data argument is a readable stream resource.
+ * The offset argument is an integer describing the offset. Contrary to
+ * what's sent in the request, the offset here is a 0-based index.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource $data
+ * @param integer $offset
+ * @return string|null
+ */
+ function putRange($data, $offset);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php
new file mode 100644
index 0000000..e6e0989
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/IPatchSupport.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace OldSabre\DAV\PartialUpdate;
+
+use OldSabre\DAV;
+
+/**
+ * This interface provides a way to modify only part of a target resource
+ * It may be used to update a file chunk, upload big a file into smaller
+ * chunks or resume an upload
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IPatchSupport extends DAV\IFile {
+
+ /**
+ * Updates the file based on a range specification.
+ *
+ * The first argument is the data, which is either a readable stream
+ * resource or a string.
+ *
+ * The second argument is the type of update we're doing.
+ * This is either:
+ * * 1. append
+ * * 2. update based on a start byte
+ * * 3. update based on an end byte
+ *;
+ * The third argument is the start or end byte.
+ *
+ * After a successful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * @param resource|string $data
+ * @param int $rangeType
+ * @param int $offset
+ * @return string|null
+ */
+ function patch($data, $rangeType, $offset = null);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php
new file mode 100644
index 0000000..c1e5363
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PartialUpdate/Plugin.php
@@ -0,0 +1,246 @@
+<?php
+
+namespace OldSabre\DAV\PartialUpdate;
+
+use OldSabre\DAV;
+
+/**
+ * Partial update plugin (Patch method)
+ *
+ * This plugin provides a way to modify only part of a target resource
+ * It may bu used to update a file chunk, upload big a file into smaller
+ * chunks or resume an upload.
+ *
+ * $patchPlugin = new \OldSabre\DAV\PartialUpdate\Plugin();
+ * $server->addPlugin($patchPlugin);
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ const RANGE_APPEND = 1;
+ const RANGE_START = 2;
+ const RANGE_END = 3;
+
+ /**
+ * Reference to server
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * Initializes the plugin
+ *
+ * This method is automatically called by the Server class after addPlugin.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'partialupdate';
+
+ }
+
+ /**
+ * This method is called by the Server if the user used an HTTP method
+ * the server didn't recognize.
+ *
+ * This plugin intercepts the PATCH methods.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool|null
+ */
+ public function unknownMethod($method, $uri) {
+
+ switch($method) {
+
+ case 'PATCH':
+ return $this->httpPatch($uri);
+
+ }
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * We claim to support PATCH method (partial update) if and only if
+ * - the node exist
+ * - the node implements our partial update interface
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ $tree = $this->server->tree;
+ if ($tree->nodeExists($uri)) {
+ $node = $tree->getNodeForPath($uri);
+ if ($node instanceof IFile || $node instanceof IPatchSupport) {
+ return array('PATCH');
+ }
+ }
+ return array();
+
+ }
+
+ /**
+ * Returns a list of features for the HTTP OPTIONS Dav: header.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('sabredav-partialupdate');
+
+ }
+
+ /**
+ * Patch an uri
+ *
+ * The WebDAV patch request can be used to modify only a part of an
+ * existing resource. If the resource does not exist yet and the first
+ * offset is not 0, the request fails
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpPatch($uri) {
+
+ // Get the node. Will throw a 404 if not found
+ $node = $this->server->tree->getNodeForPath($uri);
+ if (!$node instanceof IFile && !$node instanceof IPatchSupport) {
+ throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.');
+ }
+
+ $range = $this->getHTTPUpdateRange();
+
+ if (!$range) {
+ throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers');
+ }
+
+ $contentType = strtolower(
+ $this->server->httpRequest->getHeader('Content-Type')
+ );
+
+ if ($contentType != 'application/x-sabredav-partialupdate') {
+ throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "' . $contentType . '"');
+ }
+
+ $len = $this->server->httpRequest->getHeader('Content-Length');
+ if (!$len) throw new DAV\Exception\LengthRequired('A Content-Length header is required');
+
+ switch($range[0]) {
+ case self::RANGE_START :
+ // Calculate the end-range if it doesn't exist.
+ if (!$range[2]) {
+ $range[2] = $range[1] + $len - 1;
+ } else {
+ if ($range[2] < $range[1]) {
+ throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[2] . ') is lower than the start offset (' . $range[1] . ')');
+ }
+ if($range[2] - $range[1] + 1 != $len) {
+ throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length (' . $len . ') is not consistent with begin (' . $range[1] . ') and end (' . $range[2] . ') offsets');
+ }
+ }
+ break;
+ }
+ // Checking If-None-Match and related headers.
+ if (!$this->server->checkPreconditions()) return;
+
+ if (!$this->server->broadcastEvent('beforeWriteContent',array($uri, $node, null)))
+ return;
+
+ $body = $this->server->httpRequest->getBody();
+
+
+ if ($node instanceof IPatchSupport) {
+ $etag = $node->patch($body, $range[0], isset($range[1])?$range[1]:null);
+ } else {
+ // The old interface
+ switch($range[0]) {
+ case self::RANGE_APPEND :
+ throw new DAV\Exception\NotImplemented('This node does not support the append syntax. Please upgrade it to IPatchSupport');
+ case self::RANGE_START :
+ $etag = $node->putRange($body, $range[1]);
+ break;
+ case self::RANGE_END :
+ throw new DAV\Exception\NotImplemented('This node does not support the end-range syntax. Please upgrade it to IPatchSupport');
+ break;
+ }
+ }
+
+ $this->server->broadcastEvent('afterWriteContent',array($uri, $node));
+
+ $this->server->httpResponse->setHeader('Content-Length','0');
+ if ($etag) $this->server->httpResponse->setHeader('ETag',$etag);
+ $this->server->httpResponse->sendStatus(204);
+
+ return false;
+
+ }
+
+ /**
+ * Returns the HTTP custom range update header
+ *
+ * This method returns null if there is no well-formed HTTP range request
+ * header. It returns array(1) if it was an append request, array(2,
+ * $start, $end) if it's a start and end range, lastly it's array(3,
+ * $endoffset) if the offset was negative, and should be calculated from
+ * the end of the file.
+ *
+ * Examples:
+ *
+ * null - invalid
+ * array(1) - append
+ * array(2,10,15) - update bytes 10, 11, 12, 13, 14, 15
+ * array(2,10,null) - update bytes 10 until the end of the patch body
+ * array(3,-5) - update from 5 bytes from the end of the file.
+ *
+ * @return array|null
+ */
+ public function getHTTPUpdateRange() {
+
+ $range = $this->server->httpRequest->getHeader('X-Update-Range');
+ if (is_null($range)) return null;
+
+ // Matching "Range: bytes=1234-5678: both numbers are optional
+
+ if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i',$range,$matches)) return null;
+
+ if ($matches[1]==='append') {
+ return array(self::RANGE_APPEND);
+ } elseif (strlen($matches[2])>0) {
+ return array(self::RANGE_START, $matches[2], $matches[3]?:null);
+ } elseif ($matches[4]) {
+ return array(self::RANGE_END, $matches[4]);
+ } else {
+ return null;
+ }
+
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php
new file mode 100644
index 0000000..88500c4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Abstract property class
+ *
+ * Extend this class to create custom complex properties
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Property implements PropertyInterface {
+
+ /**
+ * Unserializes the property.
+ *
+ * This static method should return a an instance of this object.
+ *
+ * @param \DOMElement $prop
+ * @return DAV\IProperty
+ */
+ static function unserialize(\DOMElement $prop) {
+
+ throw new Exception('Unserialize has not been implemented for this class');
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php
new file mode 100644
index 0000000..33f53c2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/GetLastModified.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+use OldSabre\HTTP;
+
+/**
+ * This property represents the {DAV:}getlastmodified property.
+ *
+ * Although this is normally a simple property, windows requires us to add
+ * some new attributes.
+ *
+ * This class uses unix timestamps internally, and converts them to RFC 1123 times for
+ * serialization
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class GetLastModified extends DAV\Property {
+
+ /**
+ * time
+ *
+ * @var int
+ */
+ public $time;
+
+ /**
+ * __construct
+ *
+ * @param int|DateTime $time
+ */
+ public function __construct($time) {
+
+ if ($time instanceof \DateTime) {
+ $this->time = $time;
+ } elseif (is_int($time) || ctype_digit($time)) {
+ $this->time = new \DateTime('@' . $time);
+ } else {
+ $this->time = new \DateTime($time);
+ }
+
+ // Setting timezone to UTC
+ $this->time->setTimezone(new \DateTimeZone('UTC'));
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ $doc = $prop->ownerDocument;
+ //$prop->setAttribute('xmlns:b','urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/');
+ //$prop->setAttribute('b:dt','dateTime.rfc1123');
+ $prop->nodeValue = HTTP\Util::toHTTPDate($this->time);
+
+ }
+
+ /**
+ * getTime
+ *
+ * @return \DateTime
+ */
+ public function getTime() {
+
+ return $this->time;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php
new file mode 100644
index 0000000..81f9bfb
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Href.php
@@ -0,0 +1,99 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * Href property
+ *
+ * The href property represents a url within a {DAV:}href element.
+ * This is used by many WebDAV extensions, but not really within the WebDAV core spec
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Href extends DAV\Property implements IHref {
+
+ /**
+ * href
+ *
+ * @var string
+ */
+ private $href;
+
+ /**
+ * Automatically prefix the url with the server base directory
+ *
+ * @var bool
+ */
+ private $autoPrefix = true;
+
+ /**
+ * __construct
+ *
+ * @param string $href
+ * @param bool $autoPrefix
+ */
+ public function __construct($href, $autoPrefix = true) {
+
+ $this->href = $href;
+ $this->autoPrefix = $autoPrefix;
+
+ }
+
+ /**
+ * Returns the uri
+ *
+ * @return string
+ */
+ public function getHref() {
+
+ return $this->href;
+
+ }
+
+ /**
+ * Serializes this property.
+ *
+ * It will additionally prepend the href property with the server's base uri.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $dom) {
+
+ $prefix = $server->xmlNamespaces['DAV:'];
+ $elem = $dom->ownerDocument->createElement($prefix . ':href');
+
+ if ($this->autoPrefix) {
+ $value = $server->getBaseUri() . DAV\URLUtil::encodePath($this->href);
+ } else {
+ $value = $this->href;
+ }
+ $elem->appendChild($dom->ownerDocument->createTextNode($value));
+
+ $dom->appendChild($elem);
+
+ }
+
+ /**
+ * Unserializes this property from a DOM Element
+ *
+ * This method returns an instance of this class.
+ * It will only decode {DAV:}href values. For non-compatible elements null will be returned.
+ *
+ * @param \DOMElement $dom
+ * @return DAV\Property\Href
+ */
+ static function unserialize(\DOMElement $dom) {
+
+ if ($dom->firstChild && DAV\XMLUtil::toClarkNotation($dom->firstChild)==='{DAV:}href') {
+ return new self($dom->firstChild->textContent,false);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php
new file mode 100644
index 0000000..8233db1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/HrefList.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * HrefList property
+ *
+ * This property contains multiple {DAV:}href elements, each containing a url.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class HrefList extends DAV\Property {
+
+ /**
+ * hrefs
+ *
+ * @var array
+ */
+ private $hrefs;
+
+ /**
+ * Automatically prefix the url with the server base directory
+ *
+ * @var bool
+ */
+ private $autoPrefix = true;
+
+ /**
+ * __construct
+ *
+ * @param array $hrefs
+ * @param bool $autoPrefix
+ */
+ public function __construct(array $hrefs, $autoPrefix = true) {
+
+ $this->hrefs = $hrefs;
+ $this->autoPrefix = $autoPrefix;
+
+ }
+
+ /**
+ * Returns the uris
+ *
+ * @return array
+ */
+ public function getHrefs() {
+
+ return $this->hrefs;
+
+ }
+
+ /**
+ * Serializes this property.
+ *
+ * It will additionally prepend the href property with the server's base uri.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $dom) {
+
+ $prefix = $server->xmlNamespaces['DAV:'];
+
+ foreach($this->hrefs as $href) {
+
+ $elem = $dom->ownerDocument->createElement($prefix . ':href');
+ if ($this->autoPrefix) {
+ $value = $server->getBaseUri() . DAV\URLUtil::encodePath($href);
+ } else {
+ $value = $href;
+ }
+ $elem->appendChild($dom->ownerDocument->createTextNode($value));
+
+ $dom->appendChild($elem);
+ }
+
+ }
+
+ /**
+ * Unserializes this property from a DOM Element
+ *
+ * This method returns an instance of this class.
+ * It will only decode {DAV:}href values.
+ *
+ * @param \DOMElement $dom
+ * @return DAV\Property\HrefList
+ */
+ static function unserialize(\DOMElement $dom) {
+
+ $hrefs = array();
+ foreach($dom->childNodes as $child) {
+ if (DAV\XMLUtil::toClarkNotation($child)==='{DAV:}href') {
+ $hrefs[] = $child->textContent;
+ }
+ }
+ return new self($hrefs, false);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php
new file mode 100644
index 0000000..ce24250
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/IHref.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+/**
+ * IHref interface
+ *
+ * Any property implementing this interface can expose a related url.
+ * This is used by certain subsystems to aquire more information about for example
+ * the owner of a file
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IHref {
+
+ /**
+ * getHref
+ *
+ * @return string
+ */
+ function getHref();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php
new file mode 100644
index 0000000..0a055db
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/LockDiscovery.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * Represents {DAV:}lockdiscovery property
+ *
+ * This property contains all the open locks on a given resource
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class LockDiscovery extends DAV\Property {
+
+ /**
+ * locks
+ *
+ * @var array
+ */
+ public $locks;
+
+ /**
+ * Should we show the locktoken as well?
+ *
+ * @var bool
+ */
+ public $revealLockToken;
+
+ /**
+ * Hides the {DAV:}lockroot element from the response.
+ *
+ * It was reported that showing the lockroot in the response can break
+ * Office 2000 compatibility.
+ */
+ static public $hideLockRoot = false;
+
+ /**
+ * __construct
+ *
+ * @param array $locks
+ * @param bool $revealLockToken
+ */
+ public function __construct($locks, $revealLockToken = false) {
+
+ $this->locks = $locks;
+ $this->revealLockToken = $revealLockToken;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ $doc = $prop->ownerDocument;
+
+ foreach($this->locks as $lock) {
+
+ $activeLock = $doc->createElementNS('DAV:','d:activelock');
+ $prop->appendChild($activeLock);
+
+ $lockScope = $doc->createElementNS('DAV:','d:lockscope');
+ $activeLock->appendChild($lockScope);
+
+ $lockScope->appendChild($doc->createElementNS('DAV:','d:' . ($lock->scope==DAV\Locks\LockInfo::EXCLUSIVE?'exclusive':'shared')));
+
+ $lockType = $doc->createElementNS('DAV:','d:locktype');
+ $activeLock->appendChild($lockType);
+
+ $lockType->appendChild($doc->createElementNS('DAV:','d:write'));
+
+ /* {DAV:}lockroot */
+ if (!self::$hideLockRoot) {
+ $lockRoot = $doc->createElementNS('DAV:','d:lockroot');
+ $activeLock->appendChild($lockRoot);
+ $href = $doc->createElementNS('DAV:','d:href');
+ $href->appendChild($doc->createTextNode($server->getBaseUri() . $lock->uri));
+ $lockRoot->appendChild($href);
+ }
+
+ $activeLock->appendChild($doc->createElementNS('DAV:','d:depth',($lock->depth == DAV\Server::DEPTH_INFINITY?'infinity':$lock->depth)));
+ $activeLock->appendChild($doc->createElementNS('DAV:','d:timeout','Second-' . $lock->timeout));
+
+ if ($this->revealLockToken) {
+ $lockToken = $doc->createElementNS('DAV:','d:locktoken');
+ $activeLock->appendChild($lockToken);
+ $lockToken->appendChild($doc->createElementNS('DAV:','d:href','opaquelocktoken:' . $lock->token));
+ }
+
+ $activeLock->appendChild($doc->createElementNS('DAV:','d:owner',$lock->owner));
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php
new file mode 100644
index 0000000..eee2825
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResourceType.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * This class represents the {DAV:}resourcetype property
+ *
+ * Normally for files this is empty, and for collection {DAV:}collection.
+ * However, other specs define different values for this.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ResourceType extends DAV\Property {
+
+ /**
+ * resourceType
+ *
+ * @var array
+ */
+ public $resourceType = array();
+
+ /**
+ * __construct
+ *
+ * @param mixed $resourceType
+ */
+ public function __construct($resourceType = array()) {
+
+ if ($resourceType === DAV\Server::NODE_FILE)
+ $this->resourceType = array();
+ elseif ($resourceType === DAV\Server::NODE_DIRECTORY)
+ $this->resourceType = array('{DAV:}collection');
+ elseif (is_array($resourceType))
+ $this->resourceType = $resourceType;
+ else
+ $this->resourceType = array($resourceType);
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ $propName = null;
+ $rt = $this->resourceType;
+
+ foreach($rt as $resourceType) {
+ if (preg_match('/^{([^}]*)}(.*)$/',$resourceType,$propName)) {
+
+ if (isset($server->xmlNamespaces[$propName[1]])) {
+ $prop->appendChild($prop->ownerDocument->createElement($server->xmlNamespaces[$propName[1]] . ':' . $propName[2]));
+ } else {
+ $prop->appendChild($prop->ownerDocument->createElementNS($propName[1],'custom:' . $propName[2]));
+ }
+
+ }
+ }
+
+ }
+
+ /**
+ * Returns the values in clark-notation
+ *
+ * For example array('{DAV:}collection')
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->resourceType;
+
+ }
+
+ /**
+ * Checks if the principal contains a certain value
+ *
+ * @param string $type
+ * @return bool
+ */
+ public function is($type) {
+
+ return in_array($type, $this->resourceType);
+
+ }
+
+ /**
+ * Adds a resourcetype value to this property
+ *
+ * @param string $type
+ * @return void
+ */
+ public function add($type) {
+
+ $this->resourceType[] = $type;
+ $this->resourceType = array_unique($this->resourceType);
+
+ }
+
+ /**
+ * Unserializes a DOM element into a ResourceType property.
+ *
+ * @param \DOMElement $dom
+ * @return DAV\Property\ResourceType
+ */
+ static public function unserialize(\DOMElement $dom) {
+
+ $value = array();
+ foreach($dom->childNodes as $child) {
+
+ $value[] = DAV\XMLUtil::toClarkNotation($child);
+
+ }
+
+ return new self($value);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php
new file mode 100644
index 0000000..0e1b836
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/Response.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * Response property
+ *
+ * This class represents the {DAV:}response XML element.
+ * This is used by the Server class to encode individual items within a multistatus
+ * response.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Response extends DAV\Property implements IHref {
+
+ /**
+ * Url for the response
+ *
+ * @var string
+ */
+ private $href;
+
+ /**
+ * Propertylist, ordered by HTTP status code
+ *
+ * @var array
+ */
+ private $responseProperties;
+
+ /**
+ * The responseProperties argument is a list of properties
+ * within an array with keys representing HTTP status codes
+ *
+ * @param string $href
+ * @param array $responseProperties
+ */
+ public function __construct($href, array $responseProperties) {
+
+ $this->href = $href;
+ $this->responseProperties = $responseProperties;
+
+ }
+
+ /**
+ * Returns the url
+ *
+ * @return string
+ */
+ public function getHref() {
+
+ return $this->href;
+
+ }
+
+ /**
+ * Returns the property list
+ *
+ * @return array
+ */
+ public function getResponseProperties() {
+
+ return $this->responseProperties;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $dom) {
+
+ $document = $dom->ownerDocument;
+ $properties = $this->responseProperties;
+
+ $xresponse = $document->createElement('d:response');
+ $dom->appendChild($xresponse);
+
+ $uri = DAV\URLUtil::encodePath($this->href);
+
+ // Adding the baseurl to the beginning of the url
+ $uri = $server->getBaseUri() . $uri;
+
+ $xresponse->appendChild($document->createElement('d:href',$uri));
+
+ // The properties variable is an array containing properties, grouped by
+ // HTTP status
+ foreach($properties as $httpStatus=>$propertyGroup) {
+
+ // The 'href' is also in this array, and it's special cased.
+ // We will ignore it
+ if ($httpStatus=='href') continue;
+
+ // If there are no properties in this group, we can also just carry on
+ if (!count($propertyGroup)) continue;
+
+ $xpropstat = $document->createElement('d:propstat');
+ $xresponse->appendChild($xpropstat);
+
+ $xprop = $document->createElement('d:prop');
+ $xpropstat->appendChild($xprop);
+
+ $nsList = $server->xmlNamespaces;
+
+ foreach($propertyGroup as $propertyName=>$propertyValue) {
+
+ $propName = null;
+ preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);
+
+ // special case for empty namespaces
+ if ($propName[1]=='') {
+
+ $currentProperty = $document->createElement($propName[2]);
+ $xprop->appendChild($currentProperty);
+ $currentProperty->setAttribute('xmlns','');
+
+ } else {
+
+ if (!isset($nsList[$propName[1]])) {
+ $nsList[$propName[1]] = 'x' . count($nsList);
+ }
+
+ // If the namespace was defined in the top-level xml namespaces, it means
+ // there was already a namespace declaration, and we don't have to worry about it.
+ if (isset($server->xmlNamespaces[$propName[1]])) {
+ $currentProperty = $document->createElement($nsList[$propName[1]] . ':' . $propName[2]);
+ } else {
+ $currentProperty = $document->createElementNS($propName[1],$nsList[$propName[1]].':' . $propName[2]);
+ }
+ $xprop->appendChild($currentProperty);
+
+ }
+
+ if (is_scalar($propertyValue)) {
+ $text = $document->createTextNode($propertyValue);
+ $currentProperty->appendChild($text);
+ } elseif ($propertyValue instanceof DAV\PropertyInterface) {
+ $propertyValue->serialize($server,$currentProperty);
+ } elseif (!is_null($propertyValue)) {
+ throw new DAV\Exception('Unknown property value type: ' . gettype($propertyValue) . ' for property: ' . $propertyName);
+ }
+
+ }
+
+ $xpropstat->appendChild($document->createElement('d:status',$server->httpResponse->getStatusMessage($httpStatus)));
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php
new file mode 100644
index 0000000..ebf9296
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/ResponseList.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * ResponseList property
+ *
+ * This class represents multiple {DAV:}response XML elements.
+ * This is used by the Server class to encode items within a multistatus
+ * response.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ResponseList extends DAV\Property {
+
+ /**
+ * Response objects.
+ *
+ * @var array
+ */
+ private $responses;
+
+ /**
+ * The only valid argument is a list of OldSabre\DAV\Property\Response
+ * objects.
+ *
+ * @param array $responses;
+ */
+ public function __construct($responses) {
+
+ foreach($responses as $response) {
+ if (!($response instanceof Response)) {
+ throw new \InvalidArgumentException('You must pass an array of OldSabre\DAV\Property\Response objects');
+ }
+ }
+ $this->responses = $responses;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $dom
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $dom) {
+
+ foreach($this->responses as $response) {
+ $response->serialize($server, $dom);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php
new file mode 100644
index 0000000..1699826
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedLock.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * This class represents the {DAV:}supportedlock property
+ *
+ * This property contains information about what kind of locks
+ * this server supports.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedLock extends DAV\Property {
+
+ /**
+ * supportsLocks
+ *
+ * @var mixed
+ */
+ public $supportsLocks = false;
+
+ /**
+ * __construct
+ *
+ * @param mixed $supportsLocks
+ */
+ public function __construct($supportsLocks) {
+
+ $this->supportsLocks = $supportsLocks;
+
+ }
+
+ /**
+ * serialize
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $prop) {
+
+ $doc = $prop->ownerDocument;
+
+ if (!$this->supportsLocks) return null;
+
+ $lockEntry1 = $doc->createElement('d:lockentry');
+ $lockEntry2 = $doc->createElement('d:lockentry');
+
+ $prop->appendChild($lockEntry1);
+ $prop->appendChild($lockEntry2);
+
+ $lockScope1 = $doc->createElement('d:lockscope');
+ $lockScope2 = $doc->createElement('d:lockscope');
+ $lockType1 = $doc->createElement('d:locktype');
+ $lockType2 = $doc->createElement('d:locktype');
+
+ $lockEntry1->appendChild($lockScope1);
+ $lockEntry1->appendChild($lockType1);
+ $lockEntry2->appendChild($lockScope2);
+ $lockEntry2->appendChild($lockType2);
+
+ $lockScope1->appendChild($doc->createElement('d:exclusive'));
+ $lockScope2->appendChild($doc->createElement('d:shared'));
+
+ $lockType1->appendChild($doc->createElement('d:write'));
+ $lockType2->appendChild($doc->createElement('d:write'));
+
+ //$frag->appendXML('<d:lockentry><d:lockscope><d:exclusive /></d:lockscope><d:locktype><d:write /></d:locktype></d:lockentry>');
+ //$frag->appendXML('<d:lockentry><d:lockscope><d:shared /></d:lockscope><d:locktype><d:write /></d:locktype></d:lockentry>');
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php
new file mode 100644
index 0000000..ecbde7d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Property/SupportedReportSet.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\DAV\Property;
+
+use OldSabre\DAV;
+
+/**
+ * supported-report-set property.
+ *
+ * This property is defined in RFC3253, but since it's
+ * so common in other webdav-related specs, it is part of the core server.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedReportSet extends DAV\Property {
+
+ /**
+ * List of reports
+ *
+ * @var array
+ */
+ protected $reports = array();
+
+ /**
+ * Creates the property
+ *
+ * Any reports passed in the constructor
+ * should be valid report-types in clark-notation.
+ *
+ * Either a string or an array of strings must be passed.
+ *
+ * @param mixed $reports
+ */
+ public function __construct($reports = null) {
+
+ if (!is_null($reports))
+ $this->addReport($reports);
+
+ }
+
+ /**
+ * Adds a report to this property
+ *
+ * The report must be a string in clark-notation.
+ * Multiple reports can be specified as an array.
+ *
+ * @param mixed $report
+ * @return void
+ */
+ public function addReport($report) {
+
+ if (!is_array($report)) $report = array($report);
+
+ foreach($report as $r) {
+
+ if (!preg_match('/^{([^}]*)}(.*)$/',$r))
+ throw new DAV\Exception('Reportname must be in clark-notation');
+
+ $this->reports[] = $r;
+
+ }
+
+ }
+
+ /**
+ * Returns the list of supported reports
+ *
+ * @return array
+ */
+ public function getValue() {
+
+ return $this->reports;
+
+ }
+
+ /**
+ * Serializes the node
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $prop
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $prop) {
+
+ foreach($this->reports as $reportName) {
+
+ $supportedReport = $prop->ownerDocument->createElement('d:supported-report');
+ $prop->appendChild($supportedReport);
+
+ $report = $prop->ownerDocument->createElement('d:report');
+ $supportedReport->appendChild($report);
+
+ preg_match('/^{([^}]*)}(.*)$/',$reportName,$matches);
+
+ list(, $namespace, $element) = $matches;
+
+ $prefix = isset($server->xmlNamespaces[$namespace])?$server->xmlNamespaces[$namespace]:null;
+
+ if ($prefix) {
+ $report->appendChild($prop->ownerDocument->createElement($prefix . ':' . $element));
+ } else {
+ $report->appendChild($prop->ownerDocument->createElementNS($namespace, 'x:' . $element));
+ }
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php
new file mode 100644
index 0000000..b76f94e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/PropertyInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * PropertyInterface
+ *
+ * Implement this interface to create new complex properties
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface PropertyInterface {
+
+ public function serialize(Server $server, \DOMElement $prop);
+
+ static function unserialize(\DOMElement $prop);
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php
new file mode 100644
index 0000000..eb33f29
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Server.php
@@ -0,0 +1,2178 @@
+<?php
+
+namespace OldSabre\DAV;
+use OldSabre\HTTP;
+
+/**
+ * Main DAV server class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Server {
+
+ /**
+ * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
+ */
+ const DEPTH_INFINITY = -1;
+
+ /**
+ * Nodes that are files, should have this as the type property
+ */
+ const NODE_FILE = 1;
+
+ /**
+ * Nodes that are directories, should use this value as the type property
+ */
+ const NODE_DIRECTORY = 2;
+
+ /**
+ * XML namespace for all SabreDAV related elements
+ */
+ const NS_SABREDAV = 'http://sabredav.org/ns';
+
+ /**
+ * The tree object
+ *
+ * @var OldSabre\DAV\Tree
+ */
+ public $tree;
+
+ /**
+ * The base uri
+ *
+ * @var string
+ */
+ protected $baseUri = null;
+
+ /**
+ * httpResponse
+ *
+ * @var OldSabre\HTTP\Response
+ */
+ public $httpResponse;
+
+ /**
+ * httpRequest
+ *
+ * @var OldSabre\HTTP\Request
+ */
+ public $httpRequest;
+
+ /**
+ * The list of plugins
+ *
+ * @var array
+ */
+ protected $plugins = array();
+
+ /**
+ * This array contains a list of callbacks we should call when certain events are triggered
+ *
+ * @var array
+ */
+ protected $eventSubscriptions = array();
+
+ /**
+ * This is a default list of namespaces.
+ *
+ * If you are defining your own custom namespace, add it here to reduce
+ * bandwidth and improve legibility of xml bodies.
+ *
+ * @var array
+ */
+ public $xmlNamespaces = array(
+ 'DAV:' => 'd',
+ 'http://sabredav.org/ns' => 's',
+ );
+
+ /**
+ * The propertymap can be used to map properties from
+ * requests to property classes.
+ *
+ * @var array
+ */
+ public $propertyMap = array(
+ '{DAV:}resourcetype' => 'OldSabre\\DAV\\Property\\ResourceType',
+ );
+
+ public $protectedProperties = array(
+ // RFC4918
+ '{DAV:}getcontentlength',
+ '{DAV:}getetag',
+ '{DAV:}getlastmodified',
+ '{DAV:}lockdiscovery',
+ '{DAV:}supportedlock',
+
+ // RFC4331
+ '{DAV:}quota-available-bytes',
+ '{DAV:}quota-used-bytes',
+
+ // RFC3744
+ '{DAV:}supported-privilege-set',
+ '{DAV:}current-user-privilege-set',
+ '{DAV:}acl',
+ '{DAV:}acl-restrictions',
+ '{DAV:}inherited-acl-set',
+
+ );
+
+ /**
+ * This is a flag that allow or not showing file, line and code
+ * of the exception in the returned XML
+ *
+ * @var bool
+ */
+ public $debugExceptions = false;
+
+ /**
+ * This property allows you to automatically add the 'resourcetype' value
+ * based on a node's classname or interface.
+ *
+ * The preset ensures that {DAV:}collection is automaticlly added for nodes
+ * implementing OldSabre\DAV\ICollection.
+ *
+ * @var array
+ */
+ public $resourceTypeMapping = array(
+ 'OldSabre\\DAV\\ICollection' => '{DAV:}collection',
+ );
+
+ /**
+ * If this setting is turned off, SabreDAV's version number will be hidden
+ * from various places.
+ *
+ * Some people feel this is a good security measure.
+ *
+ * @var bool
+ */
+ static public $exposeVersion = true;
+
+ /**
+ * Sets up the server
+ *
+ * If a OldSabre\DAV\Tree object is passed as an argument, it will
+ * use it as the directory tree. If a OldSabre\DAV\INode is passed, it
+ * will create a OldSabre\DAV\ObjectTree and use the node as the root.
+ *
+ * If nothing is passed, a OldSabre\DAV\SimpleCollection is created in
+ * a OldSabre\DAV\ObjectTree.
+ *
+ * If an array is passed, we automatically create a root node, and use
+ * the nodes in the array as top-level children.
+ *
+ * @param Tree|INode|array|null $treeOrNode The tree object
+ */
+ public function __construct($treeOrNode = null) {
+
+ if ($treeOrNode instanceof Tree) {
+ $this->tree = $treeOrNode;
+ } elseif ($treeOrNode instanceof INode) {
+ $this->tree = new ObjectTree($treeOrNode);
+ } elseif (is_array($treeOrNode)) {
+
+ // If it's an array, a list of nodes was passed, and we need to
+ // create the root node.
+ foreach($treeOrNode as $node) {
+ if (!($node instanceof INode)) {
+ throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement OldSabre\\DAV\\INode');
+ }
+ }
+
+ $root = new SimpleCollection('root', $treeOrNode);
+ $this->tree = new ObjectTree($root);
+
+ } elseif (is_null($treeOrNode)) {
+ $root = new SimpleCollection('root');
+ $this->tree = new ObjectTree($root);
+ } else {
+ throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of OldSabre\\DAV\\Tree, OldSabre\\DAV\\INode, an array or null');
+ }
+ $this->httpResponse = new HTTP\Response();
+ $this->httpRequest = new HTTP\Request();
+
+ }
+
+ /**
+ * Starts the DAV Server
+ *
+ * @return void
+ */
+ public function exec() {
+
+ try {
+
+ // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
+ // origin, we must make sure we send back HTTP/1.0 if this was
+ // requested.
+ // This is mainly because nginx doesn't support Chunked Transfer
+ // Encoding, and this forces the webserver SabreDAV is running on,
+ // to buffer entire responses to calculate Content-Length.
+ $this->httpResponse->defaultHttpVersion = $this->httpRequest->getHTTPVersion();
+
+ $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
+
+ } catch (Exception $e) {
+
+ try {
+ $this->broadcastEvent('exception', array($e));
+ } catch (Exception $ignore) {
+ }
+ $DOM = new \DOMDocument('1.0','utf-8');
+ $DOM->formatOutput = true;
+
+ $error = $DOM->createElementNS('DAV:','d:error');
+ $error->setAttribute('xmlns:s',self::NS_SABREDAV);
+ $DOM->appendChild($error);
+
+ $h = function($v) {
+
+ return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8');
+
+ };
+
+ $error->appendChild($DOM->createElement('s:exception',$h(get_class($e))));
+ $error->appendChild($DOM->createElement('s:message',$h($e->getMessage())));
+ if ($this->debugExceptions) {
+ $error->appendChild($DOM->createElement('s:file',$h($e->getFile())));
+ $error->appendChild($DOM->createElement('s:line',$h($e->getLine())));
+ $error->appendChild($DOM->createElement('s:code',$h($e->getCode())));
+ $error->appendChild($DOM->createElement('s:stacktrace',$h($e->getTraceAsString())));
+
+ }
+ if (self::$exposeVersion) {
+ $error->appendChild($DOM->createElement('s:sabredav-version',$h(Version::VERSION)));
+ }
+
+ if($e instanceof Exception) {
+
+ $httpCode = $e->getHTTPCode();
+ $e->serialize($this,$error);
+ $headers = $e->getHTTPHeaders($this);
+
+ } else {
+
+ $httpCode = 500;
+ $headers = array();
+
+ }
+ $headers['Content-Type'] = 'application/xml; charset=utf-8';
+
+ $this->httpResponse->sendStatus($httpCode);
+ $this->httpResponse->setHeaders($headers);
+ $this->httpResponse->sendBody($DOM->saveXML());
+
+ }
+
+ }
+
+ /**
+ * Sets the base server uri
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function setBaseUri($uri) {
+
+ // If the baseUri does not end with a slash, we must add it
+ if ($uri[strlen($uri)-1]!=='/')
+ $uri.='/';
+
+ $this->baseUri = $uri;
+
+ }
+
+ /**
+ * Returns the base responding uri
+ *
+ * @return string
+ */
+ public function getBaseUri() {
+
+ if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
+ return $this->baseUri;
+
+ }
+
+ /**
+ * This method attempts to detect the base uri.
+ * Only the PATH_INFO variable is considered.
+ *
+ * If this variable is not set, the root (/) is assumed.
+ *
+ * @return string
+ */
+ public function guessBaseUri() {
+
+ $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
+ $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
+
+ // If PATH_INFO is found, we can assume it's accurate.
+ if (!empty($pathInfo)) {
+
+ // We need to make sure we ignore the QUERY_STRING part
+ if ($pos = strpos($uri,'?'))
+ $uri = substr($uri,0,$pos);
+
+ // PATH_INFO is only set for urls, such as: /example.php/path
+ // in that case PATH_INFO contains '/path'.
+ // Note that REQUEST_URI is percent encoded, while PATH_INFO is
+ // not, Therefore they are only comparable if we first decode
+ // REQUEST_INFO as well.
+ $decodedUri = URLUtil::decodePath($uri);
+
+ // A simple sanity check:
+ if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
+ $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
+ return rtrim($baseUri,'/') . '/';
+ }
+
+ throw new Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
+
+ }
+
+ // The last fallback is that we're just going to assume the server root.
+ return '/';
+
+ }
+
+ /**
+ * Adds a plugin to the server
+ *
+ * For more information, console the documentation of OldSabre\DAV\ServerPlugin
+ *
+ * @param ServerPlugin $plugin
+ * @return void
+ */
+ public function addPlugin(ServerPlugin $plugin) {
+
+ $this->plugins[$plugin->getPluginName()] = $plugin;
+ $plugin->initialize($this);
+
+ }
+
+ /**
+ * Returns an initialized plugin by it's name.
+ *
+ * This function returns null if the plugin was not found.
+ *
+ * @param string $name
+ * @return ServerPlugin
+ */
+ public function getPlugin($name) {
+
+ if (isset($this->plugins[$name]))
+ return $this->plugins[$name];
+
+ // This is a fallback and deprecated.
+ foreach($this->plugins as $plugin) {
+ if (get_class($plugin)===$name) return $plugin;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Returns all plugins
+ *
+ * @return array
+ */
+ public function getPlugins() {
+
+ return $this->plugins;
+
+ }
+
+
+ /**
+ * Subscribe to an event.
+ *
+ * When the event is triggered, we'll call all the specified callbacks.
+ * It is possible to control the order of the callbacks through the
+ * priority argument.
+ *
+ * This is for example used to make sure that the authentication plugin
+ * is triggered before anything else. If it's not needed to change this
+ * number, it is recommended to ommit.
+ *
+ * @param string $event
+ * @param callback $callback
+ * @param int $priority
+ * @return void
+ */
+ public function subscribeEvent($event, $callback, $priority = 100) {
+
+ if (!isset($this->eventSubscriptions[$event])) {
+ $this->eventSubscriptions[$event] = array();
+ }
+ while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
+ $this->eventSubscriptions[$event][$priority] = $callback;
+ ksort($this->eventSubscriptions[$event]);
+
+ }
+
+ /**
+ * Broadcasts an event
+ *
+ * This method will call all subscribers. If one of the subscribers returns false, the process stops.
+ *
+ * The arguments parameter will be sent to all subscribers
+ *
+ * @param string $eventName
+ * @param array $arguments
+ * @return bool
+ */
+ public function broadcastEvent($eventName,$arguments = array()) {
+
+ if (isset($this->eventSubscriptions[$eventName])) {
+
+ foreach($this->eventSubscriptions[$eventName] as $subscriber) {
+
+ $result = call_user_func_array($subscriber,$arguments);
+ if ($result===false) return false;
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+ /**
+ * Handles a http request, and execute a method based on its name
+ *
+ * @param string $method
+ * @param string $uri
+ * @return void
+ */
+ public function invokeMethod($method, $uri) {
+
+ $method = strtoupper($method);
+
+ if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
+
+ // Make sure this is a HTTP method we support
+ $internalMethods = array(
+ 'OPTIONS',
+ 'GET',
+ 'HEAD',
+ 'DELETE',
+ 'PROPFIND',
+ 'MKCOL',
+ 'PUT',
+ 'PROPPATCH',
+ 'COPY',
+ 'MOVE',
+ 'REPORT'
+ );
+
+ if (in_array($method,$internalMethods)) {
+
+ call_user_func(array($this,'http' . $method), $uri);
+
+ } else {
+
+ if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
+ // Unsupported method
+ throw new Exception\NotImplemented('There was no handler found for this "' . $method . '" method');
+ }
+
+ }
+
+ }
+
+ // {{{ HTTP Method implementations
+
+ /**
+ * HTTP OPTIONS
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpOptions($uri) {
+
+ $methods = $this->getAllowedMethods($uri);
+
+ $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
+ $features = array('1','3', 'extended-mkcol');
+
+ foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
+
+ $this->httpResponse->setHeader('DAV',implode(', ',$features));
+ $this->httpResponse->setHeader('MS-Author-Via','DAV');
+ $this->httpResponse->setHeader('Accept-Ranges','bytes');
+ if (self::$exposeVersion) {
+ $this->httpResponse->setHeader('X-Sabre-Version',Version::VERSION);
+ }
+ $this->httpResponse->setHeader('Content-Length',0);
+ $this->httpResponse->sendStatus(200);
+
+ }
+
+ /**
+ * HTTP GET
+ *
+ * This method simply fetches the contents of a uri, like normal
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpGet($uri) {
+
+ $node = $this->tree->getNodeForPath($uri,0);
+
+ if (!$this->checkPreconditions(true)) return false;
+ if (!$node instanceof IFile) throw new Exception\NotImplemented('GET is only implemented on File objects');
+
+ $body = $node->get();
+
+ // Converting string into stream, if needed.
+ if (is_string($body)) {
+ $stream = fopen('php://temp','r+');
+ fwrite($stream,$body);
+ rewind($stream);
+ $body = $stream;
+ }
+
+ /*
+ * TODO: getetag, getlastmodified, getsize should also be used using
+ * this method
+ */
+ $httpHeaders = $this->getHTTPHeaders($uri);
+
+ /* ContentType needs to get a default, because many webservers will otherwise
+ * default to text/html, and we don't want this for security reasons.
+ */
+ if (!isset($httpHeaders['Content-Type'])) {
+ $httpHeaders['Content-Type'] = 'application/octet-stream';
+ }
+
+
+ if (isset($httpHeaders['Content-Length'])) {
+
+ $nodeSize = $httpHeaders['Content-Length'];
+
+ // Need to unset Content-Length, because we'll handle that during figuring out the range
+ unset($httpHeaders['Content-Length']);
+
+ } else {
+ $nodeSize = null;
+ }
+
+ $this->httpResponse->setHeaders($httpHeaders);
+
+ $range = $this->getHTTPRange();
+ $ifRange = $this->httpRequest->getHeader('If-Range');
+ $ignoreRangeHeader = false;
+
+ // If ifRange is set, and range is specified, we first need to check
+ // the precondition.
+ if ($nodeSize && $range && $ifRange) {
+
+ // if IfRange is parsable as a date we'll treat it as a DateTime
+ // otherwise, we must treat it as an etag.
+ try {
+ $ifRangeDate = new \DateTime($ifRange);
+
+ // It's a date. We must check if the entity is modified since
+ // the specified date.
+ if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
+ else {
+ $modified = new \DateTime($httpHeaders['Last-Modified']);
+ if($modified > $ifRangeDate) $ignoreRangeHeader = true;
+ }
+
+ } catch (\Exception $e) {
+
+ // It's an entity. We can do a simple comparison.
+ if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
+ elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
+ }
+ }
+
+ // We're only going to support HTTP ranges if the backend provided a filesize
+ if (!$ignoreRangeHeader && $nodeSize && $range) {
+
+ // Determining the exact byte offsets
+ if (!is_null($range[0])) {
+
+ $start = $range[0];
+ $end = $range[1]?$range[1]:$nodeSize-1;
+ if($start >= $nodeSize)
+ throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
+
+ if($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
+ if($end >= $nodeSize) $end = $nodeSize-1;
+
+ } else {
+
+ $start = $nodeSize-$range[1];
+ $end = $nodeSize-1;
+
+ if ($start<0) $start = 0;
+
+ }
+
+ // New read/write stream
+ $newStream = fopen('php://temp','r+');
+
+ // stream_copy_to_stream() has a bug/feature: the `whence` argument
+ // is interpreted as SEEK_SET (count from absolute offset 0), while
+ // for a stream it should be SEEK_CUR (count from current offset).
+ // If a stream is nonseekable, the function fails. So we *emulate*
+ // the correct behaviour with fseek():
+ if ($start > 0) {
+ if (($curOffs = ftell($body)) === false) $curOffs = 0;
+ fseek($body, $start - $curOffs, SEEK_CUR);
+ }
+ stream_copy_to_stream($body, $newStream, $end-$start+1);
+ rewind($newStream);
+
+ $this->httpResponse->setHeader('Content-Length', $end-$start+1);
+ $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
+ $this->httpResponse->sendStatus(206);
+ $this->httpResponse->sendBody($newStream);
+
+
+ } else {
+
+ if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
+ $this->httpResponse->sendStatus(200);
+ $this->httpResponse->sendBody($body);
+
+ }
+
+ }
+
+ /**
+ * HTTP HEAD
+ *
+ * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body
+ * This is used by clients to determine if a remote file was changed, so they can use a local cached version, instead of downloading it again
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpHead($uri) {
+
+ $node = $this->tree->getNodeForPath($uri);
+ /* This information is only collection for File objects.
+ * Ideally we want to throw 405 Method Not Allowed for every
+ * non-file, but MS Office does not like this
+ */
+ if ($node instanceof IFile) {
+ $headers = $this->getHTTPHeaders($this->getRequestUri());
+ if (!isset($headers['Content-Type'])) {
+ $headers['Content-Type'] = 'application/octet-stream';
+ }
+ $this->httpResponse->setHeaders($headers);
+ }
+ $this->httpResponse->sendStatus(200);
+
+ }
+
+ /**
+ * HTTP Delete
+ *
+ * The HTTP delete method, deletes a given uri
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpDelete($uri) {
+
+ // Checking If-None-Match and related headers.
+ if (!$this->checkPreconditions()) return;
+
+ if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
+ $this->tree->delete($uri);
+ $this->broadcastEvent('afterUnbind',array($uri));
+
+ $this->httpResponse->sendStatus(204);
+ $this->httpResponse->setHeader('Content-Length','0');
+
+ }
+
+
+ /**
+ * WebDAV PROPFIND
+ *
+ * This WebDAV method requests information about an uri resource, or a list of resources
+ * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
+ * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
+ *
+ * The request body contains an XML data structure that has a list of properties the client understands
+ * The response body is also an xml document, containing information about every uri resource and the requested properties
+ *
+ * It has to return a HTTP 207 Multi-status status code
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpPropfind($uri) {
+
+ $requestedProperties = $this->parsePropFindRequest($this->httpRequest->getBody(true));
+
+ $depth = $this->getHTTPDepth(1);
+ // The only two options for the depth of a propfind is 0 or 1
+ if ($depth!=0) $depth = 1;
+
+ $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
+
+ // This is a multi-status response
+ $this->httpResponse->sendStatus(207);
+ $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->httpResponse->setHeader('Vary','Brief,Prefer');
+
+ // Normally this header is only needed for OPTIONS responses, however..
+ // iCal seems to also depend on these being set for PROPFIND. Since
+ // this is not harmful, we'll add it.
+ $features = array('1','3', 'extended-mkcol');
+ foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
+ $this->httpResponse->setHeader('DAV',implode(', ',$features));
+
+ $prefer = $this->getHTTPPrefer();
+ $minimal = $prefer['return-minimal'];
+
+ $data = $this->generateMultiStatus($newProperties, $minimal);
+ $this->httpResponse->sendBody($data);
+
+ }
+
+ /**
+ * WebDAV PROPPATCH
+ *
+ * This method is called to update properties on a Node. The request is an XML body with all the mutations.
+ * In this XML body it is specified which properties should be set/updated and/or deleted
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpPropPatch($uri) {
+
+ $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
+
+ $result = $this->updateProperties($uri, $newProperties);
+
+ $prefer = $this->getHTTPPrefer();
+ $this->httpResponse->setHeader('Vary','Brief,Prefer');
+
+ if ($prefer['return-minimal']) {
+
+ // If return-minimal is specified, we only have to check if the
+ // request was succesful, and don't need to return the
+ // multi-status.
+ $ok = true;
+ foreach($result as $code=>$prop) {
+ if ((int)$code > 299) {
+ $ok = false;
+ }
+ }
+
+ if ($ok) {
+
+ $this->httpResponse->sendStatus(204);
+ return;
+
+ }
+
+ }
+
+ $this->httpResponse->sendStatus(207);
+ $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+
+ $this->httpResponse->sendBody(
+ $this->generateMultiStatus(array($result))
+ );
+
+ }
+
+ /**
+ * HTTP PUT method
+ *
+ * This HTTP method updates a file, or creates a new one.
+ *
+ * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpPut($uri) {
+
+ $body = $this->httpRequest->getBody();
+
+ // Intercepting Content-Range
+ if ($this->httpRequest->getHeader('Content-Range')) {
+ /**
+ Content-Range is dangerous for PUT requests: PUT per definition
+ stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
+ in section 7.6:
+ An origin server SHOULD reject any PUT request that contains a
+ Content-Range header field, since it might be misinterpreted as
+ partial content (or might be partial content that is being mistakenly
+ PUT as a full representation). Partial content updates are possible
+ by targeting a separately identified resource with state that
+ overlaps a portion of the larger resource, or by using a different
+ method that has been specifically defined for partial updates (for
+ example, the PATCH method defined in [RFC5789]).
+ This clarifies RFC2616 section 9.6:
+ The recipient of the entity MUST NOT ignore any Content-*
+ (e.g. Content-Range) headers that it does not understand or implement
+ and MUST return a 501 (Not Implemented) response in such cases.
+ OTOH is a PUT request with a Content-Range currently the only way to
+ continue an aborted upload request and is supported by curl, mod_dav,
+ Tomcat and others. Since some clients do use this feature which results
+ in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
+ all PUT requests with a Content-Range for now.
+ */
+
+ throw new Exception\NotImplemented('PUT with Content-Range is not allowed.');
+ }
+
+ // Intercepting the Finder problem
+ if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
+
+ /**
+ Many webservers will not cooperate well with Finder PUT requests,
+ because it uses 'Chunked' transfer encoding for the request body.
+
+ The symptom of this problem is that Finder sends files to the
+ server, but they arrive as 0-length files in PHP.
+
+ If we don't do anything, the user might think they are uploading
+ files successfully, but they end up empty on the server. Instead,
+ we throw back an error if we detect this.
+
+ The reason Finder uses Chunked, is because it thinks the files
+ might change as it's being uploaded, and therefore the
+ Content-Length can vary.
+
+ Instead it sends the X-Expected-Entity-Length header with the size
+ of the file at the very start of the request. If this header is set,
+ but we don't get a request body we will fail the request to
+ protect the end-user.
+ */
+
+ // Only reading first byte
+ $firstByte = fread($body,1);
+ if (strlen($firstByte)!==1) {
+ throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
+ }
+
+ // The body needs to stay intact, so we copy everything to a
+ // temporary stream.
+
+ $newBody = fopen('php://temp','r+');
+ fwrite($newBody,$firstByte);
+ stream_copy_to_stream($body, $newBody);
+ rewind($newBody);
+
+ $body = $newBody;
+
+ }
+
+ // Checking If-None-Match and related headers.
+ if (!$this->checkPreconditions()) return;
+
+ if ($this->tree->nodeExists($uri)) {
+
+ $node = $this->tree->getNodeForPath($uri);
+
+ // If the node is a collection, we'll deny it
+ if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.');
+ if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
+
+ $etag = $node->put($body);
+
+ $this->broadcastEvent('afterWriteContent',array($uri, $node));
+
+ $this->httpResponse->setHeader('Content-Length','0');
+ if ($etag) $this->httpResponse->setHeader('ETag',$etag);
+ $this->httpResponse->sendStatus(204);
+
+ } else {
+
+ $etag = null;
+ // If we got here, the resource didn't exist yet.
+ if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
+ // For one reason or another the file was not created.
+ return;
+ }
+
+ $this->httpResponse->setHeader('Content-Length','0');
+ if ($etag) $this->httpResponse->setHeader('ETag', $etag);
+ $this->httpResponse->sendStatus(201);
+
+ }
+
+ }
+
+
+ /**
+ * WebDAV MKCOL
+ *
+ * The MKCOL method is used to create a new collection (directory) on the server
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpMkcol($uri) {
+
+ $requestBody = $this->httpRequest->getBody(true);
+
+ if ($requestBody) {
+
+ $contentType = $this->httpRequest->getHeader('Content-Type');
+ if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
+
+ // We must throw 415 for unsupported mkcol bodies
+ throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
+
+ }
+
+ $dom = XMLUtil::loadDOMDocument($requestBody);
+ if (XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
+
+ // We must throw 415 for unsupported mkcol bodies
+ throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
+
+ }
+
+ $properties = array();
+ foreach($dom->firstChild->childNodes as $childNode) {
+
+ if (XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
+ $properties = array_merge($properties, XMLUtil::parseProperties($childNode, $this->propertyMap));
+
+ }
+ if (!isset($properties['{DAV:}resourcetype']))
+ throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
+
+ $resourceType = $properties['{DAV:}resourcetype']->getValue();
+ unset($properties['{DAV:}resourcetype']);
+
+ } else {
+
+ $properties = array();
+ $resourceType = array('{DAV:}collection');
+
+ }
+
+ $result = $this->createCollection($uri, $resourceType, $properties);
+
+ if (is_array($result)) {
+ $this->httpResponse->sendStatus(207);
+ $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+
+ $this->httpResponse->sendBody(
+ $this->generateMultiStatus(array($result))
+ );
+
+ } else {
+ $this->httpResponse->setHeader('Content-Length','0');
+ $this->httpResponse->sendStatus(201);
+ }
+
+ }
+
+ /**
+ * WebDAV HTTP MOVE method
+ *
+ * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpMove($uri) {
+
+ $moveInfo = $this->getCopyAndMoveInfo();
+
+ // If the destination is part of the source tree, we must fail
+ if ($moveInfo['destination']==$uri)
+ throw new Exception\Forbidden('Source and destination uri are identical.');
+
+ if ($moveInfo['destinationExists']) {
+
+ if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
+ $this->tree->delete($moveInfo['destination']);
+ $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
+
+ }
+
+ if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
+ if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
+ $this->tree->move($uri,$moveInfo['destination']);
+ $this->broadcastEvent('afterUnbind',array($uri));
+ $this->broadcastEvent('afterBind',array($moveInfo['destination']));
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $this->httpResponse->setHeader('Content-Length','0');
+ $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
+
+ }
+
+ /**
+ * WebDAV HTTP COPY method
+ *
+ * This method copies one uri to a different uri, and works much like the MOVE request
+ * A lot of the actual request processing is done in getCopyMoveInfo
+ *
+ * @param string $uri
+ * @return bool
+ */
+ protected function httpCopy($uri) {
+
+ $copyInfo = $this->getCopyAndMoveInfo();
+ // If the destination is part of the source tree, we must fail
+ if ($copyInfo['destination']==$uri)
+ throw new Exception\Forbidden('Source and destination uri are identical.');
+
+ if ($copyInfo['destinationExists']) {
+ if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
+ $this->tree->delete($copyInfo['destination']);
+
+ }
+ if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
+ $this->tree->copy($uri,$copyInfo['destination']);
+ $this->broadcastEvent('afterBind',array($copyInfo['destination']));
+
+ // If a resource was overwritten we should send a 204, otherwise a 201
+ $this->httpResponse->setHeader('Content-Length','0');
+ $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
+
+ }
+
+
+
+ /**
+ * HTTP REPORT method implementation
+ *
+ * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
+ * It's used in a lot of extensions, so it made sense to implement it into the core.
+ *
+ * @param string $uri
+ * @return void
+ */
+ protected function httpReport($uri) {
+
+ $body = $this->httpRequest->getBody(true);
+ $dom = XMLUtil::loadDOMDocument($body);
+
+ $reportName = XMLUtil::toClarkNotation($dom->firstChild);
+
+ if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
+
+ // If broadcastEvent returned true, it means the report was not supported
+ throw new Exception\ReportNotSupported();
+
+ }
+
+ }
+
+ // }}}
+ // {{{ HTTP/WebDAV protocol helpers
+
+ /**
+ * Returns an array with all the supported HTTP methods for a specific uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getAllowedMethods($uri) {
+
+ $methods = array(
+ 'OPTIONS',
+ 'GET',
+ 'HEAD',
+ 'DELETE',
+ 'PROPFIND',
+ 'PUT',
+ 'PROPPATCH',
+ 'COPY',
+ 'MOVE',
+ 'REPORT'
+ );
+
+ // The MKCOL is only allowed on an unmapped uri
+ try {
+ $this->tree->getNodeForPath($uri);
+ } catch (Exception\NotFound $e) {
+ $methods[] = 'MKCOL';
+ }
+
+ // We're also checking if any of the plugins register any new methods
+ foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
+ array_unique($methods);
+
+ return $methods;
+
+ }
+
+ /**
+ * Gets the uri for the request, keeping the base uri into consideration
+ *
+ * @return string
+ */
+ public function getRequestUri() {
+
+ return $this->calculateUri($this->httpRequest->getUri());
+
+ }
+
+ /**
+ * Calculates the uri for a request, making sure that the base uri is stripped out
+ *
+ * @param string $uri
+ * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
+ * @return string
+ */
+ public function calculateUri($uri) {
+
+ if ($uri[0]!='/' && strpos($uri,'://')) {
+
+ $uri = parse_url($uri,PHP_URL_PATH);
+
+ }
+
+ $uri = str_replace('//','/',$uri);
+
+ if (strpos($uri,$this->getBaseUri())===0) {
+
+ return trim(URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
+
+ // A special case, if the baseUri was accessed without a trailing
+ // slash, we'll accept it as well.
+ } elseif ($uri.'/' === $this->getBaseUri()) {
+
+ return '';
+
+ } else {
+
+ throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
+
+ }
+
+ }
+
+ /**
+ * Returns the HTTP depth header
+ *
+ * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the OldSabre\DAV\Server::DEPTH_INFINITY object
+ * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
+ *
+ * @param mixed $default
+ * @return int
+ */
+ public function getHTTPDepth($default = self::DEPTH_INFINITY) {
+
+ // If its not set, we'll grab the default
+ $depth = $this->httpRequest->getHeader('Depth');
+
+ if (is_null($depth)) return $default;
+
+ if ($depth == 'infinity') return self::DEPTH_INFINITY;
+
+
+ // If its an unknown value. we'll grab the default
+ if (!ctype_digit($depth)) return $default;
+
+ return (int)$depth;
+
+ }
+
+ /**
+ * Returns the HTTP range header
+ *
+ * This method returns null if there is no well-formed HTTP range request
+ * header or array($start, $end).
+ *
+ * The first number is the offset of the first byte in the range.
+ * The second number is the offset of the last byte in the range.
+ *
+ * If the second offset is null, it should be treated as the offset of the last byte of the entity
+ * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
+ *
+ * @return array|null
+ */
+ public function getHTTPRange() {
+
+ $range = $this->httpRequest->getHeader('range');
+ if (is_null($range)) return null;
+
+ // Matching "Range: bytes=1234-5678: both numbers are optional
+
+ if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
+
+ if ($matches[1]==='' && $matches[2]==='') return null;
+
+ return array(
+ $matches[1]!==''?$matches[1]:null,
+ $matches[2]!==''?$matches[2]:null,
+ );
+
+ }
+
+ /**
+ * Returns the HTTP Prefer header information.
+ *
+ * The prefer header is defined in:
+ * http://tools.ietf.org/html/draft-snell-http-prefer-14
+ *
+ * This method will return an array with options.
+ *
+ * Currently, the following options may be returned:
+ * array(
+ * 'return-asynch' => true,
+ * 'return-minimal' => true,
+ * 'return-representation' => true,
+ * 'wait' => 30,
+ * 'strict' => true,
+ * 'lenient' => true,
+ * )
+ *
+ * This method also supports the Brief header, and will also return
+ * 'return-minimal' if the brief header was set to 't'.
+ *
+ * For the boolean options, false will be returned if the headers are not
+ * specified. For the integer options it will be 'null'.
+ *
+ * @return array
+ */
+ public function getHTTPPrefer() {
+
+ $result = array(
+ 'return-asynch' => false,
+ 'return-minimal' => false,
+ 'return-representation' => false,
+ 'wait' => null,
+ 'strict' => false,
+ 'lenient' => false,
+ );
+
+ if ($prefer = $this->httpRequest->getHeader('Prefer')) {
+
+ $parameters = array_map('trim',
+ explode(',', $prefer)
+ );
+
+ foreach($parameters as $parameter) {
+
+ // Right now our regex only supports the tokens actually
+ // specified in the draft. We may need to expand this if new
+ // tokens get registered.
+ if(!preg_match('/^(?P<token>[a-z0-9-]+)(?:=(?P<value>[0-9]+))?$/', $parameter, $matches)) {
+ continue;
+ }
+
+ switch($matches['token']) {
+
+ case 'return-asynch' :
+ case 'return-minimal' :
+ case 'return-representation' :
+ case 'strict' :
+ case 'lenient' :
+ $result[$matches['token']] = true;
+ break;
+ case 'wait' :
+ $result[$matches['token']] = $matches['value'];
+ break;
+
+ }
+
+ }
+
+ }
+
+ if ($this->httpRequest->getHeader('Brief')=='t') {
+ $result['return-minimal'] = true;
+ }
+
+ return $result;
+
+ }
+
+
+ /**
+ * Returns information about Copy and Move requests
+ *
+ * This function is created to help getting information about the source and the destination for the
+ * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
+ *
+ * The returned value is an array with the following keys:
+ * * destination - Destination path
+ * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
+ *
+ * @return array
+ */
+ public function getCopyAndMoveInfo() {
+
+ // Collecting the relevant HTTP headers
+ if (!$this->httpRequest->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied');
+ $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
+ $overwrite = $this->httpRequest->getHeader('Overwrite');
+ if (!$overwrite) $overwrite = 'T';
+ if (strtoupper($overwrite)=='T') $overwrite = true;
+ elseif (strtoupper($overwrite)=='F') $overwrite = false;
+ // We need to throw a bad request exception, if the header was invalid
+ else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
+
+ list($destinationDir) = URLUtil::splitPath($destination);
+
+ try {
+ $destinationParent = $this->tree->getNodeForPath($destinationDir);
+ if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection');
+ } catch (Exception\NotFound $e) {
+
+ // If the destination parent node is not found, we throw a 409
+ throw new Exception\Conflict('The destination node is not found');
+ }
+
+ try {
+
+ $destinationNode = $this->tree->getNodeForPath($destination);
+
+ // If this succeeded, it means the destination already exists
+ // we'll need to throw precondition failed in case overwrite is false
+ if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
+
+ } catch (Exception\NotFound $e) {
+
+ // Destination didn't exist, we're all good
+ $destinationNode = false;
+
+
+
+ }
+
+ // These are the three relevant properties we need to return
+ return array(
+ 'destination' => $destination,
+ 'destinationExists' => $destinationNode==true,
+ 'destinationNode' => $destinationNode,
+ );
+
+ }
+
+ /**
+ * Returns a list of properties for a path
+ *
+ * This is a simplified version getPropertiesForPath.
+ * if you aren't interested in status codes, but you just
+ * want to have a flat list of properties. Use this method.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ */
+ public function getProperties($path, $propertyNames) {
+
+ $result = $this->getPropertiesForPath($path,$propertyNames,0);
+ return $result[0][200];
+
+ }
+
+ /**
+ * A kid-friendly way to fetch properties for a node's children.
+ *
+ * The returned array will be indexed by the path of the of child node.
+ * Only properties that are actually found will be returned.
+ *
+ * The parent node will not be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ * @return array
+ */
+ public function getPropertiesForChildren($path, $propertyNames) {
+
+ $result = array();
+ foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
+
+ // Skipping the parent path
+ if ($k === 0) continue;
+
+ $result[$row['href']] = $row[200];
+
+ }
+ return $result;
+
+ }
+
+ /**
+ * Returns a list of HTTP headers for a particular resource
+ *
+ * The generated http headers are based on properties provided by the
+ * resource. The method basically provides a simple mapping between
+ * DAV property and HTTP header.
+ *
+ * The headers are intended to be used for HEAD and GET requests.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getHTTPHeaders($path) {
+
+ $propertyMap = array(
+ '{DAV:}getcontenttype' => 'Content-Type',
+ '{DAV:}getcontentlength' => 'Content-Length',
+ '{DAV:}getlastmodified' => 'Last-Modified',
+ '{DAV:}getetag' => 'ETag',
+ );
+
+ $properties = $this->getProperties($path,array_keys($propertyMap));
+
+ $headers = array();
+ foreach($propertyMap as $property=>$header) {
+ if (!isset($properties[$property])) continue;
+
+ if (is_scalar($properties[$property])) {
+ $headers[$header] = $properties[$property];
+
+ // GetLastModified gets special cased
+ } elseif ($properties[$property] instanceof Property\GetLastModified) {
+ $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime());
+ }
+
+ }
+
+ return $headers;
+
+ }
+
+ /**
+ * Returns a list of properties for a given path
+ *
+ * The path that should be supplied should have the baseUrl stripped out
+ * The list of properties should be supplied in Clark notation. If the list is empty
+ * 'allprops' is assumed.
+ *
+ * If a depth of 1 is requested child elements will also be returned.
+ *
+ * @param string $path
+ * @param array $propertyNames
+ * @param int $depth
+ * @return array
+ */
+ public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
+
+ if ($depth!=0) $depth = 1;
+
+ $path = rtrim($path,'/');
+
+ // This event allows people to intercept these requests early on in the
+ // process.
+ //
+ // We're not doing anything with the result, but this can be helpful to
+ // pre-fetch certain expensive live properties.
+ $this->broadCastEvent('beforeGetPropertiesForPath', array($path, $propertyNames, $depth));
+
+ $returnPropertyList = array();
+
+ $parentNode = $this->tree->getNodeForPath($path);
+ $nodes = array(
+ $path => $parentNode
+ );
+ if ($depth==1 && $parentNode instanceof ICollection) {
+ foreach($this->tree->getChildren($path) as $childNode)
+ $nodes[$path . '/' . $childNode->getName()] = $childNode;
+ }
+
+ // If the propertyNames array is empty, it means all properties are requested.
+ // We shouldn't actually return everything we know though, and only return a
+ // sensible list.
+ $allProperties = count($propertyNames)==0;
+
+ foreach($nodes as $myPath=>$node) {
+
+ $currentPropertyNames = $propertyNames;
+
+ $newProperties = array(
+ '200' => array(),
+ '404' => array(),
+ );
+
+ if ($allProperties) {
+ // Default list of propertyNames, when all properties were requested.
+ $currentPropertyNames = array(
+ '{DAV:}getlastmodified',
+ '{DAV:}getcontentlength',
+ '{DAV:}resourcetype',
+ '{DAV:}quota-used-bytes',
+ '{DAV:}quota-available-bytes',
+ '{DAV:}getetag',
+ '{DAV:}getcontenttype',
+ );
+ }
+
+ // If the resourceType was not part of the list, we manually add it
+ // and mark it for removal. We need to know the resourcetype in order
+ // to make certain decisions about the entry.
+ // WebDAV dictates we should add a / and the end of href's for collections
+ $removeRT = false;
+ if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
+ $currentPropertyNames[] = '{DAV:}resourcetype';
+ $removeRT = true;
+ }
+
+ $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
+ // If this method explicitly returned false, we must ignore this
+ // node as it is inaccessible.
+ if ($result===false) continue;
+
+ if (count($currentPropertyNames) > 0) {
+
+ if ($node instanceof IProperties) {
+ $nodeProperties = $node->getProperties($currentPropertyNames);
+
+ // The getProperties method may give us too much,
+ // properties, in case the implementor was lazy.
+ //
+ // So as we loop through this list, we will only take the
+ // properties that were actually requested and discard the
+ // rest.
+ foreach($currentPropertyNames as $k=>$currentPropertyName) {
+ if (isset($nodeProperties[$currentPropertyName])) {
+ unset($currentPropertyNames[$k]);
+ $newProperties[200][$currentPropertyName] = $nodeProperties[$currentPropertyName];
+ }
+ }
+
+ }
+
+ }
+
+ foreach($currentPropertyNames as $prop) {
+
+ if (isset($newProperties[200][$prop])) continue;
+
+ switch($prop) {
+ case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Property\GetLastModified($node->getLastModified()); break;
+ case '{DAV:}getcontentlength' :
+ if ($node instanceof IFile) {
+ $size = $node->getSize();
+ if (!is_null($size)) {
+ $newProperties[200][$prop] = (int)$node->getSize();
+ }
+ }
+ break;
+ case '{DAV:}quota-used-bytes' :
+ if ($node instanceof IQuota) {
+ $quotaInfo = $node->getQuotaInfo();
+ $newProperties[200][$prop] = $quotaInfo[0];
+ }
+ break;
+ case '{DAV:}quota-available-bytes' :
+ if ($node instanceof IQuota) {
+ $quotaInfo = $node->getQuotaInfo();
+ $newProperties[200][$prop] = $quotaInfo[1];
+ }
+ break;
+ case '{DAV:}getetag' : if ($node instanceof IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
+ case '{DAV:}getcontenttype' : if ($node instanceof IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break;
+ case '{DAV:}supported-report-set' :
+ $reports = array();
+ foreach($this->plugins as $plugin) {
+ $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
+ }
+ $newProperties[200][$prop] = new Property\SupportedReportSet($reports);
+ break;
+ case '{DAV:}resourcetype' :
+ $newProperties[200]['{DAV:}resourcetype'] = new Property\ResourceType();
+ foreach($this->resourceTypeMapping as $className => $resourceType) {
+ if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
+ }
+ break;
+
+ }
+
+ // If we were unable to find the property, we will list it as 404.
+ if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
+
+ }
+
+ $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties, $node));
+
+ $newProperties['href'] = trim($myPath,'/');
+
+ // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
+ // Apple's iCal also requires a trailing slash for principals (rfc 3744), though this is non-standard.
+ if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype'])) {
+ $rt = $newProperties[200]['{DAV:}resourcetype'];
+ if ($rt->is('{DAV:}collection') || $rt->is('{DAV:}principal')) {
+ $newProperties['href'] .='/';
+ }
+ }
+
+ // If the resourcetype property was manually added to the requested property list,
+ // we will remove it again.
+ if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
+
+ $returnPropertyList[] = $newProperties;
+
+ }
+
+ return $returnPropertyList;
+
+ }
+
+ /**
+ * This method is invoked by sub-systems creating a new file.
+ *
+ * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
+ * It was important to get this done through a centralized function,
+ * allowing plugins to intercept this using the beforeCreateFile event.
+ *
+ * This method will return true if the file was actually created
+ *
+ * @param string $uri
+ * @param resource $data
+ * @param string $etag
+ * @return bool
+ */
+ public function createFile($uri,$data, &$etag = null) {
+
+ list($dir,$name) = URLUtil::splitPath($uri);
+
+ if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
+
+ $parent = $this->tree->getNodeForPath($dir);
+ if (!$parent instanceof ICollection) {
+ throw new Exception\Conflict('Files can only be created as children of collections');
+ }
+
+ if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
+
+ $etag = $parent->createFile($name,$data);
+ $this->tree->markDirty($dir . '/' . $name);
+
+ $this->broadcastEvent('afterBind',array($uri));
+ $this->broadcastEvent('afterCreateFile',array($uri, $parent));
+
+ return true;
+ }
+
+ /**
+ * This method is invoked by sub-systems creating a new directory.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function createDirectory($uri) {
+
+ $this->createCollection($uri,array('{DAV:}collection'),array());
+
+ }
+
+ /**
+ * Use this method to create a new collection
+ *
+ * The {DAV:}resourcetype is specified using the resourceType array.
+ * At the very least it must contain {DAV:}collection.
+ *
+ * The properties array can contain a list of additional properties.
+ *
+ * @param string $uri The new uri
+ * @param array $resourceType The resourceType(s)
+ * @param array $properties A list of properties
+ * @return array|null
+ */
+ public function createCollection($uri, array $resourceType, array $properties) {
+
+ list($parentUri,$newName) = URLUtil::splitPath($uri);
+
+ // Making sure {DAV:}collection was specified as resourceType
+ if (!in_array('{DAV:}collection', $resourceType)) {
+ throw new Exception\InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
+ }
+
+
+ // Making sure the parent exists
+ try {
+
+ $parent = $this->tree->getNodeForPath($parentUri);
+
+ } catch (Exception\NotFound $e) {
+
+ throw new Exception\Conflict('Parent node does not exist');
+
+ }
+
+ // Making sure the parent is a collection
+ if (!$parent instanceof ICollection) {
+ throw new Exception\Conflict('Parent node is not a collection');
+ }
+
+
+
+ // Making sure the child does not already exist
+ try {
+ $parent->getChild($newName);
+
+ // If we got here.. it means there's already a node on that url, and we need to throw a 405
+ throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
+
+ } catch (Exception\NotFound $e) {
+ // This is correct
+ }
+
+
+ if (!$this->broadcastEvent('beforeBind',array($uri))) return;
+
+ // There are 2 modes of operation. The standard collection
+ // creates the directory, and then updates properties
+ // the extended collection can create it directly.
+ if ($parent instanceof IExtendedCollection) {
+
+ $parent->createExtendedCollection($newName, $resourceType, $properties);
+
+ } else {
+
+ // No special resourcetypes are supported
+ if (count($resourceType)>1) {
+ throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
+ }
+
+ $parent->createDirectory($newName);
+ $rollBack = false;
+ $exception = null;
+ $errorResult = null;
+
+ if (count($properties)>0) {
+
+ try {
+
+ $errorResult = $this->updateProperties($uri, $properties);
+ if (!isset($errorResult[200])) {
+ $rollBack = true;
+ }
+
+ } catch (Exception $e) {
+
+ $rollBack = true;
+ $exception = $e;
+
+ }
+
+ }
+
+ if ($rollBack) {
+ if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
+ $this->tree->delete($uri);
+
+ // Re-throwing exception
+ if ($exception) throw $exception;
+
+ return $errorResult;
+ }
+
+ }
+ $this->tree->markDirty($parentUri);
+ $this->broadcastEvent('afterBind',array($uri));
+
+ }
+
+ /**
+ * This method updates a resource's properties
+ *
+ * The properties array must be a list of properties. Array-keys are
+ * property names in clarknotation, array-values are it's values.
+ * If a property must be deleted, the value should be null.
+ *
+ * Note that this request should either completely succeed, or
+ * completely fail.
+ *
+ * The response is an array with statuscodes for keys, which in turn
+ * contain arrays with propertynames. This response can be used
+ * to generate a multistatus body.
+ *
+ * @param string $uri
+ * @param array $properties
+ * @return array
+ */
+ public function updateProperties($uri, array $properties) {
+
+ // we'll start by grabbing the node, this will throw the appropriate
+ // exceptions if it doesn't.
+ $node = $this->tree->getNodeForPath($uri);
+
+ $result = array(
+ 200 => array(),
+ 403 => array(),
+ 424 => array(),
+ );
+ $remainingProperties = $properties;
+ $hasError = false;
+
+ // Running through all properties to make sure none of them are protected
+ if (!$hasError) foreach($properties as $propertyName => $value) {
+ if(in_array($propertyName, $this->protectedProperties)) {
+ $result[403][$propertyName] = null;
+ unset($remainingProperties[$propertyName]);
+ $hasError = true;
+ }
+ }
+
+ if (!$hasError) {
+ // Allowing plugins to take care of property updating
+ $hasError = !$this->broadcastEvent('updateProperties',array(
+ &$remainingProperties,
+ &$result,
+ $node
+ ));
+ }
+
+ // If the node is not an instance of OldSabre\DAV\IProperties, every
+ // property is 403 Forbidden
+ if (!$hasError && count($remainingProperties) && !($node instanceof IProperties)) {
+ $hasError = true;
+ foreach($properties as $propertyName=> $value) {
+ $result[403][$propertyName] = null;
+ }
+ $remainingProperties = array();
+ }
+
+ // Only if there were no errors we may attempt to update the resource
+ if (!$hasError) {
+
+ if (count($remainingProperties)>0) {
+
+ $updateResult = $node->updateProperties($remainingProperties);
+
+ if ($updateResult===true) {
+ // success
+ foreach($remainingProperties as $propertyName=>$value) {
+ $result[200][$propertyName] = null;
+ }
+
+ } elseif ($updateResult===false) {
+ // The node failed to update the properties for an
+ // unknown reason
+ foreach($remainingProperties as $propertyName=>$value) {
+ $result[403][$propertyName] = null;
+ }
+
+ } elseif (is_array($updateResult)) {
+
+ // The node has detailed update information
+ // We need to merge the results with the earlier results.
+ foreach($updateResult as $status => $props) {
+ if (is_array($props)) {
+ if (!isset($result[$status]))
+ $result[$status] = array();
+
+ $result[$status] = array_merge($result[$status], $updateResult[$status]);
+ }
+ }
+
+ } else {
+ throw new Exception('Invalid result from updateProperties');
+ }
+ $remainingProperties = array();
+ }
+
+ }
+
+ foreach($remainingProperties as $propertyName=>$value) {
+ // if there are remaining properties, it must mean
+ // there's a dependency failure
+ $result[424][$propertyName] = null;
+ }
+
+ // Removing empty array values
+ foreach($result as $status=>$props) {
+
+ if (count($props)===0) unset($result[$status]);
+
+ }
+ $result['href'] = $uri;
+ return $result;
+
+ }
+
+ /**
+ * This method checks the main HTTP preconditions.
+ *
+ * Currently these are:
+ * * If-Match
+ * * If-None-Match
+ * * If-Modified-Since
+ * * If-Unmodified-Since
+ *
+ * The method will return true if all preconditions are met
+ * The method will return false, or throw an exception if preconditions
+ * failed. If false is returned the operation should be aborted, and
+ * the appropriate HTTP response headers are already set.
+ *
+ * Normally this method will throw 412 Precondition Failed for failures
+ * related to If-None-Match, If-Match and If-Unmodified Since. It will
+ * set the status to 304 Not Modified for If-Modified_since.
+ *
+ * If the $handleAsGET argument is set to true, it will also return 304
+ * Not Modified for failure of the If-None-Match precondition. This is the
+ * desired behaviour for HTTP GET and HTTP HEAD requests.
+ *
+ * @param bool $handleAsGET
+ * @return bool
+ */
+ public function checkPreconditions($handleAsGET = false) {
+
+ $uri = $this->getRequestUri();
+ $node = null;
+ $lastMod = null;
+ $etag = null;
+
+ if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
+
+ // If-Match contains an entity tag. Only if the entity-tag
+ // matches we are allowed to make the request succeed.
+ // If the entity-tag is '*' we are only allowed to make the
+ // request succeed if a resource exists at that url.
+ try {
+ $node = $this->tree->getNodeForPath($uri);
+ } catch (Exception\NotFound $e) {
+ throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
+ }
+
+ // Only need to check entity tags if they are not *
+ if ($ifMatch!=='*') {
+
+ // There can be multiple etags
+ $ifMatch = explode(',',$ifMatch);
+ $haveMatch = false;
+ foreach($ifMatch as $ifMatchItem) {
+
+ // Stripping any extra spaces
+ $ifMatchItem = trim($ifMatchItem,' ');
+
+ $etag = $node->getETag();
+ if ($etag===$ifMatchItem) {
+ $haveMatch = true;
+ } else {
+ // Evolution has a bug where it sometimes prepends the "
+ // with a \. This is our workaround.
+ if (str_replace('\\"','"', $ifMatchItem) === $etag) {
+ $haveMatch = true;
+ }
+ }
+
+ }
+ if (!$haveMatch) {
+ throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
+ }
+ }
+ }
+
+ if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
+
+ // The If-None-Match header contains an etag.
+ // Only if the ETag does not match the current ETag, the request will succeed
+ // The header can also contain *, in which case the request
+ // will only succeed if the entity does not exist at all.
+ $nodeExists = true;
+ if (!$node) {
+ try {
+ $node = $this->tree->getNodeForPath($uri);
+ } catch (Exception\NotFound $e) {
+ $nodeExists = false;
+ }
+ }
+ if ($nodeExists) {
+ $haveMatch = false;
+ if ($ifNoneMatch==='*') $haveMatch = true;
+ else {
+
+ // There might be multiple etags
+ $ifNoneMatch = explode(',', $ifNoneMatch);
+ $etag = $node->getETag();
+
+ foreach($ifNoneMatch as $ifNoneMatchItem) {
+
+ // Stripping any extra spaces
+ $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
+
+ if ($etag===$ifNoneMatchItem) $haveMatch = true;
+
+ }
+
+ }
+
+ if ($haveMatch) {
+ if ($handleAsGET) {
+ $this->httpResponse->sendStatus(304);
+ return false;
+ } else {
+ throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
+ }
+ }
+ }
+
+ }
+
+ if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
+
+ // The If-Modified-Since header contains a date. We
+ // will only return the entity if it has been changed since
+ // that date. If it hasn't been changed, we return a 304
+ // header
+ // Note that this header only has to be checked if there was no If-None-Match header
+ // as per the HTTP spec.
+ $date = HTTP\Util::parseHTTPDate($ifModifiedSince);
+
+ if ($date) {
+ if (is_null($node)) {
+ $node = $this->tree->getNodeForPath($uri);
+ }
+ $lastMod = $node->getLastModified();
+ if ($lastMod) {
+ $lastMod = new \DateTime('@' . $lastMod);
+ if ($lastMod <= $date) {
+ $this->httpResponse->sendStatus(304);
+ $this->httpResponse->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod));
+ return false;
+ }
+ }
+ }
+ }
+
+ if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
+
+ // The If-Unmodified-Since will allow allow the request if the
+ // entity has not changed since the specified date.
+ $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince);
+
+ // We must only check the date if it's valid
+ if ($date) {
+ if (is_null($node)) {
+ $node = $this->tree->getNodeForPath($uri);
+ }
+ $lastMod = $node->getLastModified();
+ if ($lastMod) {
+ $lastMod = new \DateTime('@' . $lastMod);
+ if ($lastMod > $date) {
+ throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
+ }
+ }
+ }
+
+ }
+ return true;
+
+ }
+
+ // }}}
+ // {{{ XML Readers & Writers
+
+
+ /**
+ * Generates a WebDAV propfind response body based on a list of nodes.
+ *
+ * If 'strip404s' is set to true, all 404 responses will be removed.
+ *
+ * @param array $fileProperties The list with nodes
+ * @param bool strip404s
+ * @return string
+ */
+ public function generateMultiStatus(array $fileProperties, $strip404s = false) {
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ //$dom->formatOutput = true;
+ $multiStatus = $dom->createElement('d:multistatus');
+ $dom->appendChild($multiStatus);
+
+ // Adding in default namespaces
+ foreach($this->xmlNamespaces as $namespace=>$prefix) {
+
+ $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+
+ foreach($fileProperties as $entry) {
+
+ $href = $entry['href'];
+ unset($entry['href']);
+
+ if ($strip404s && isset($entry[404])) {
+ unset($entry[404]);
+ }
+
+ $response = new Property\Response($href,$entry);
+ $response->serialize($this,$multiStatus);
+
+ }
+
+ return $dom->saveXML();
+
+ }
+
+ /**
+ * This method parses a PropPatch request
+ *
+ * PropPatch changes the properties for a resource. This method
+ * returns a list of properties.
+ *
+ * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
+ * and the value contains the property value. If a property is to be removed the value
+ * will be null.
+ *
+ * @param string $body xml body
+ * @return array list of properties in need of updating or deletion
+ */
+ public function parsePropPatchRequest($body) {
+
+ //We'll need to change the DAV namespace declaration to something else in order to make it parsable
+ $dom = XMLUtil::loadDOMDocument($body);
+
+ $newProperties = array();
+
+ foreach($dom->firstChild->childNodes as $child) {
+
+ if ($child->nodeType !== XML_ELEMENT_NODE) continue;
+
+ $operation = XMLUtil::toClarkNotation($child);
+
+ if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
+
+ $innerProperties = XMLUtil::parseProperties($child, $this->propertyMap);
+
+ foreach($innerProperties as $propertyName=>$propertyValue) {
+
+ if ($operation==='{DAV:}remove') {
+ $propertyValue = null;
+ }
+
+ $newProperties[$propertyName] = $propertyValue;
+
+ }
+
+ }
+
+ return $newProperties;
+
+ }
+
+ /**
+ * This method parses the PROPFIND request and returns its information
+ *
+ * This will either be a list of properties, or an empty array; in which case
+ * an {DAV:}allprop was requested.
+ *
+ * @param string $body
+ * @return array
+ */
+ public function parsePropFindRequest($body) {
+
+ // If the propfind body was empty, it means IE is requesting 'all' properties
+ if (!$body) return array();
+
+ $dom = XMLUtil::loadDOMDocument($body);
+ $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
+ return array_keys(XMLUtil::parseProperties($elem));
+
+ }
+
+ // }}}
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php
new file mode 100644
index 0000000..a9061a3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/ServerPlugin.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * The baseclass for all server plugins.
+ *
+ * Plugins can modify or extend the servers behaviour.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class ServerPlugin {
+
+ /**
+ * This initializes the plugin.
+ *
+ * This function is called by OldSabre\DAV\Server, after
+ * addPlugin is called.
+ *
+ * This method should set up the required event subscriptions.
+ *
+ * @param Server $server
+ * @return void
+ */
+ abstract public function initialize(Server $server);
+
+ /**
+ * This method should return a list of server-features.
+ *
+ * This is for example 'versioning' and is added to the DAV: header
+ * in an OPTIONS response.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array();
+
+ }
+
+ /**
+ * Use this method to tell the server this plugin defines additional
+ * HTTP methods.
+ *
+ * This method is passed a uri. It should only return HTTP methods that are
+ * available for the specified uri.
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getHTTPMethods($uri) {
+
+ return array();
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using \OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return get_class($this);
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ return array();
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php
new file mode 100644
index 0000000..959096d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleCollection.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * SimpleCollection
+ *
+ * The SimpleCollection is used to quickly setup static directory structures.
+ * Just create the object with a proper name, and add children to use it.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SimpleCollection extends Collection {
+
+ /**
+ * List of childnodes
+ *
+ * @var array
+ */
+ protected $children = array();
+
+ /**
+ * Name of this resource
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Creates this node
+ *
+ * The name of the node must be passed, child nodes can also be passed.
+ * This nodes must be instances of INode
+ *
+ * @param string $name
+ * @param array $children
+ */
+ public function __construct($name,array $children = array()) {
+
+ $this->name = $name;
+ foreach($children as $child) {
+
+ if (!($child instanceof INode)) throw new Exception('Only instances of OldSabre\DAV\INode are allowed to be passed in the children argument');
+ $this->addChild($child);
+
+ }
+
+ }
+
+ /**
+ * Adds a new childnode to this collection
+ *
+ * @param INode $child
+ * @return void
+ */
+ public function addChild(INode $child) {
+
+ $this->children[$child->getName()] = $child;
+
+ }
+
+ /**
+ * Returns the name of the collection
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->name;
+
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * This method makes use of the getChildren method to grab all the child nodes, and compares the name.
+ * Generally its wise to override this, as this can usually be optimized
+ *
+ * This method must throw OldSabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws Exception\NotFound
+ * @return INode
+ */
+ public function getChild($name) {
+
+ if (isset($this->children[$name])) return $this->children[$name];
+ throw new Exception\NotFound('File not found: ' . $name . ' in \'' . $this->getName() . '\'');
+
+ }
+
+ /**
+ * Returns a list of children for this collection
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ return array_values($this->children);
+
+ }
+
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php
new file mode 100644
index 0000000..956d020
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/SimpleFile.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * SimpleFile
+ *
+ * The 'SimpleFile' class is used to easily add read-only immutable files to
+ * the directory structure. One usecase would be to add a 'readme.txt' to a
+ * root of a webserver with some standard content.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SimpleFile extends File {
+
+ /**
+ * File contents
+ *
+ * @var string
+ */
+ protected $contents = array();
+
+ /**
+ * Name of this resource
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * A mimetype, such as 'text/plain' or 'text/html'
+ *
+ * @var string
+ */
+ protected $mimeType;
+
+ /**
+ * Creates this node
+ *
+ * The name of the node must be passed, as well as the contents of the
+ * file.
+ *
+ * @param string $name
+ * @param string $contents
+ * @param string|null $mimeType
+ */
+ public function __construct($name, $contents, $mimeType = null) {
+
+ $this->name = $name;
+ $this->contents = $contents;
+ $this->mimeType = $mimeType;
+
+ }
+
+ /**
+ * Returns the node name for this file.
+ *
+ * This name is used to construct the url.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ return $this->name;
+
+ }
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ public function get() {
+
+ return $this->contents;
+
+ }
+
+ /**
+ * Returns the size of the file, in bytes.
+ *
+ * @return int
+ */
+ public function getSize() {
+
+ return strlen($this->contents);
+
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ * @return string
+ */
+ public function getETag() {
+
+ return '"' . md5($this->contents) . '"';
+
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ * @return string
+ */
+ public function getContentType() {
+
+ return $this->mimeType;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php
new file mode 100644
index 0000000..57e35c4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/StringUtil.php
@@ -0,0 +1,91 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * String utility
+ *
+ * This class is mainly used to implement the 'text-match' filter, used by both
+ * the CalDAV calendar-query REPORT, and CardDAV addressbook-query REPORT.
+ * Because they both need it, it was decided to put it in OldSabre\DAV instead.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class StringUtil {
+
+ /**
+ * Checks if a needle occurs in a haystack ;)
+ *
+ * @param string $haystack
+ * @param string $needle
+ * @param string $collation
+ * @param string $matchType
+ * @return bool
+ */
+ static public function textMatch($haystack, $needle, $collation, $matchType = 'contains') {
+
+ switch($collation) {
+
+ case 'i;ascii-casemap' :
+ // default strtolower takes locale into consideration
+ // we don't want this.
+ $haystack = str_replace(range('a','z'), range('A','Z'), $haystack);
+ $needle = str_replace(range('a','z'), range('A','Z'), $needle);
+ break;
+
+ case 'i;octet' :
+ // Do nothing
+ break;
+
+ case 'i;unicode-casemap' :
+ $haystack = mb_strtoupper($haystack, 'UTF-8');
+ $needle = mb_strtoupper($needle, 'UTF-8');
+ break;
+
+ default :
+ throw new Exception\BadRequest('Collation type: ' . $collation . ' is not supported');
+
+ }
+
+ switch($matchType) {
+
+ case 'contains' :
+ return strpos($haystack, $needle)!==false;
+ case 'equals' :
+ return $haystack === $needle;
+ case 'starts-with' :
+ return strpos($haystack, $needle)===0;
+ case 'ends-with' :
+ return strrpos($haystack, $needle)===strlen($haystack)-strlen($needle);
+ default :
+ throw new Exception\BadRequest('Match-type: ' . $matchType . ' is not supported');
+
+ }
+
+ }
+
+ /**
+ * This method takes an input string, checks if it's not valid UTF-8 and
+ * attempts to convert it to UTF-8 if it's not.
+ *
+ * Note that currently this can only convert ISO-8559-1 to UTF-8 (latin-1),
+ * anything else will likely fail.
+ *
+ * @param string $input
+ * @return string
+ */
+ static public function ensureUTF8($input) {
+
+ $encoding = mb_detect_encoding($input , array('UTF-8','ISO-8859-1'), true);
+
+ if ($encoding === 'ISO-8859-1') {
+ return utf8_encode($input);
+ } else {
+ return $input;
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php
new file mode 100644
index 0000000..5a2a2f0
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/TemporaryFileFilterPlugin.php
@@ -0,0 +1,289 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Temporary File Filter Plugin
+ *
+ * The purpose of this filter is to intercept some of the garbage files
+ * operation systems and applications tend to generate when mounting
+ * a WebDAV share as a disk.
+ *
+ * It will intercept these files and place them in a separate directory.
+ * these files are not deleted automatically, so it is adviceable to
+ * delete these after they are not accessed for 24 hours.
+ *
+ * Currently it supports:
+ * * OS/X style resource forks and .DS_Store
+ * * desktop.ini and Thumbs.db (windows)
+ * * .*.swp (vim temporary files)
+ * * .dat.* (smultron temporary files)
+ *
+ * Additional patterns can be added, by adding on to the
+ * temporaryFilePatterns property.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class TemporaryFileFilterPlugin extends ServerPlugin {
+
+ /**
+ * This is the list of patterns we intercept.
+ * If new patterns are added, they must be valid patterns for preg_match.
+ *
+ * @var array
+ */
+ public $temporaryFilePatterns = array(
+ '/^\._(.*)$/', // OS/X resource forks
+ '/^.DS_Store$/', // OS/X custom folder settings
+ '/^desktop.ini$/', // Windows custom folder settings
+ '/^Thumbs.db$/', // Windows thumbnail cache
+ '/^.(.*).swp$/', // ViM temporary files
+ '/^\.dat(.*)$/', // Smultron seems to create these
+ '/^~lock.(.*)#$/', // Windows 7 lockfiles
+ );
+
+ /**
+ * A reference to the main Server class
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * This is the directory where this plugin
+ * will store it's files.
+ *
+ * @var string
+ */
+ private $dataDir;
+
+ /**
+ * Creates the plugin.
+ *
+ * Make sure you specify a directory for your files. If you don't, we
+ * will use PHP's directory for session-storage instead, and you might
+ * not want that.
+ *
+ * @param string|null $dataDir
+ */
+ public function __construct($dataDir = null) {
+
+ if (!$dataDir) $dataDir = ini_get('session.save_path').'/sabredav/';
+ if (!is_dir($dataDir)) mkdir($dataDir);
+ $this->dataDir = $dataDir;
+
+ }
+
+ /**
+ * Initialize the plugin
+ *
+ * This is called automatically be the Server class after this plugin is
+ * added with OldSabre\DAV\Server::addPlugin()
+ *
+ * @param Server $server
+ * @return void
+ */
+ public function initialize(Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('beforeMethod',array($this,'beforeMethod'));
+ $server->subscribeEvent('beforeCreateFile',array($this,'beforeCreateFile'));
+
+ }
+
+ /**
+ * This method is called before any HTTP method handler
+ *
+ * This method intercepts any GET, DELETE, PUT and PROPFIND calls to
+ * filenames that are known to match the 'temporary file' regex.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function beforeMethod($method, $uri) {
+
+ if (!$tempLocation = $this->isTempFile($uri))
+ return true;
+
+ switch($method) {
+ case 'GET' :
+ return $this->httpGet($tempLocation);
+ case 'PUT' :
+ return $this->httpPut($tempLocation);
+ case 'PROPFIND' :
+ return $this->httpPropfind($tempLocation, $uri);
+ case 'DELETE' :
+ return $this->httpDelete($tempLocation);
+ }
+ return true;
+
+ }
+
+ /**
+ * This method is invoked if some subsystem creates a new file.
+ *
+ * This is used to deal with HTTP LOCK requests which create a new
+ * file.
+ *
+ * @param string $uri
+ * @param resource $data
+ * @return bool
+ */
+ public function beforeCreateFile($uri,$data) {
+
+ if ($tempPath = $this->isTempFile($uri)) {
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+ file_put_contents($tempPath,$data);
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * This method will check if the url matches the temporary file pattern
+ * if it does, it will return an path based on $this->dataDir for the
+ * temporary file storage.
+ *
+ * @param string $path
+ * @return boolean|string
+ */
+ protected function isTempFile($path) {
+
+ // We're only interested in the basename.
+ list(, $tempPath) = URLUtil::splitPath($path);
+
+ foreach($this->temporaryFilePatterns as $tempFile) {
+
+ if (preg_match($tempFile,$tempPath)) {
+ return $this->getDataDir() . '/sabredav_' . md5($path) . '.tempfile';
+ }
+
+ }
+
+ return false;
+
+ }
+
+
+ /**
+ * This method handles the GET method for temporary files.
+ * If the file doesn't exist, it will return false which will kick in
+ * the regular system for the GET method.
+ *
+ * @param string $tempLocation
+ * @return bool
+ */
+ public function httpGet($tempLocation) {
+
+ if (!file_exists($tempLocation)) return true;
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('Content-Type','application/octet-stream');
+ $hR->setHeader('Content-Length',filesize($tempLocation));
+ $hR->setHeader('X-Sabre-Temp','true');
+ $hR->sendStatus(200);
+ $hR->sendBody(fopen($tempLocation,'r'));
+ return false;
+
+ }
+
+ /**
+ * This method handles the PUT method.
+ *
+ * @param string $tempLocation
+ * @return bool
+ */
+ public function httpPut($tempLocation) {
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+
+ $newFile = !file_exists($tempLocation);
+
+ if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) {
+ throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied');
+ }
+
+ file_put_contents($tempLocation,$this->server->httpRequest->getBody());
+ $hR->sendStatus($newFile?201:200);
+ return false;
+
+ }
+
+ /**
+ * This method handles the DELETE method.
+ *
+ * If the file didn't exist, it will return false, which will make the
+ * standard HTTP DELETE handler kick in.
+ *
+ * @param string $tempLocation
+ * @return bool
+ */
+ public function httpDelete($tempLocation) {
+
+ if (!file_exists($tempLocation)) return true;
+
+ unlink($tempLocation);
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+ $hR->sendStatus(204);
+ return false;
+
+ }
+
+ /**
+ * This method handles the PROPFIND method.
+ *
+ * It's a very lazy method, it won't bother checking the request body
+ * for which properties were requested, and just sends back a default
+ * set of properties.
+ *
+ * @param string $tempLocation
+ * @param string $uri
+ * @return bool
+ */
+ public function httpPropfind($tempLocation, $uri) {
+
+ if (!file_exists($tempLocation)) return true;
+
+ $hR = $this->server->httpResponse;
+ $hR->setHeader('X-Sabre-Temp','true');
+ $hR->sendStatus(207);
+ $hR->setHeader('Content-Type','application/xml; charset=utf-8');
+
+ $this->server->parsePropFindRequest($this->server->httpRequest->getBody(true));
+
+ $properties = array(
+ 'href' => $uri,
+ 200 => array(
+ '{DAV:}getlastmodified' => new Property\GetLastModified(filemtime($tempLocation)),
+ '{DAV:}getcontentlength' => filesize($tempLocation),
+ '{DAV:}resourcetype' => new Property\ResourceType(null),
+ '{'.Server::NS_SABREDAV.'}tempFile' => true,
+
+ ),
+ );
+
+ $data = $this->server->generateMultiStatus(array($properties));
+ $hR->sendBody($data);
+ return false;
+
+ }
+
+
+ /**
+ * This method returns the directory where the temporary files should be stored.
+ *
+ * @return string
+ */
+ protected function getDataDir()
+ {
+ return $this->dataDir;
+ }
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php
new file mode 100644
index 0000000..8313918
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree.php
@@ -0,0 +1,193 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * Abstract tree object
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class Tree {
+
+ /**
+ * This function must return an INode object for a path
+ * If a Path doesn't exist, thrown a Exception_NotFound
+ *
+ * @param string $path
+ * @throws Exception\NotFound
+ * @return INode
+ */
+ abstract function getNodeForPath($path);
+
+ /**
+ * This function allows you to check if a node exists.
+ *
+ * Implementors of this class should override this method to make
+ * it cheaper.
+ *
+ * @param string $path
+ * @return bool
+ */
+ public function nodeExists($path) {
+
+ try {
+
+ $this->getNodeForPath($path);
+ return true;
+
+ } catch (Exception\NotFound $e) {
+
+ return false;
+
+ }
+
+ }
+
+ /**
+ * Copies a file from path to another
+ *
+ * @param string $sourcePath The source location
+ * @param string $destinationPath The full destination path
+ * @return void
+ */
+ public function copy($sourcePath, $destinationPath) {
+
+ $sourceNode = $this->getNodeForPath($sourcePath);
+
+ // grab the dirname and basename components
+ list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath);
+
+ $destinationParent = $this->getNodeForPath($destinationDir);
+ $this->copyNode($sourceNode,$destinationParent,$destinationName);
+
+ $this->markDirty($destinationDir);
+
+ }
+
+ /**
+ * Moves a file from one location to another
+ *
+ * @param string $sourcePath The path to the file which should be moved
+ * @param string $destinationPath The full destination path, so not just the destination parent node
+ * @return int
+ */
+ public function move($sourcePath, $destinationPath) {
+
+ list($sourceDir, $sourceName) = URLUtil::splitPath($sourcePath);
+ list($destinationDir, $destinationName) = URLUtil::splitPath($destinationPath);
+
+ if ($sourceDir===$destinationDir) {
+ $renameable = $this->getNodeForPath($sourcePath);
+ $renameable->setName($destinationName);
+ } else {
+ $this->copy($sourcePath,$destinationPath);
+ $this->getNodeForPath($sourcePath)->delete();
+ }
+ $this->markDirty($sourceDir);
+ $this->markDirty($destinationDir);
+
+ }
+
+ /**
+ * Deletes a node from the tree
+ *
+ * @param string $path
+ * @return void
+ */
+ public function delete($path) {
+
+ $node = $this->getNodeForPath($path);
+ $node->delete();
+
+ list($parent) = URLUtil::splitPath($path);
+ $this->markDirty($parent);
+
+ }
+
+ /**
+ * Returns a list of childnodes for a given path.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getChildren($path) {
+
+ $node = $this->getNodeForPath($path);
+ return $node->getChildren();
+
+ }
+
+ /**
+ * This method is called with every tree update
+ *
+ * Examples of tree updates are:
+ * * node deletions
+ * * node creations
+ * * copy
+ * * move
+ * * renaming nodes
+ *
+ * If Tree classes implement a form of caching, this will allow
+ * them to make sure caches will be expired.
+ *
+ * If a path is passed, it is assumed that the entire subtree is dirty
+ *
+ * @param string $path
+ * @return void
+ */
+ public function markDirty($path) {
+
+
+ }
+
+ /**
+ * copyNode
+ *
+ * @param INode $source
+ * @param ICollection $destinationParent
+ * @param string $destinationName
+ * @return void
+ */
+ protected function copyNode(INode $source,ICollection $destinationParent,$destinationName = null) {
+
+ if (!$destinationName) $destinationName = $source->getName();
+
+ if ($source instanceof IFile) {
+
+ $data = $source->get();
+
+ // If the body was a string, we need to convert it to a stream
+ if (is_string($data)) {
+ $stream = fopen('php://temp','r+');
+ fwrite($stream,$data);
+ rewind($stream);
+ $data = $stream;
+ }
+ $destinationParent->createFile($destinationName,$data);
+ $destination = $destinationParent->getChild($destinationName);
+
+ } elseif ($source instanceof ICollection) {
+
+ $destinationParent->createDirectory($destinationName);
+
+ $destination = $destinationParent->getChild($destinationName);
+ foreach($source->getChildren() as $child) {
+
+ $this->copyNode($child,$destination);
+
+ }
+
+ }
+ if ($source instanceof IProperties && $destination instanceof IProperties) {
+
+ $props = $source->getProperties(array());
+ $destination->updateProperties($props);
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php
new file mode 100644
index 0000000..9d1a638
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Tree/Filesystem.php
@@ -0,0 +1,133 @@
+<?php
+
+namespace OldSabre\DAV\Tree;
+
+use OldSabre\DAV;
+
+/**
+ * FileSystem Tree
+ *
+ * This class is an alternative to the standard ObjectTree. This tree can only
+ * use OldSabre\DAV\FS\Directory and File classes, but as a result it allows for a few
+ * optimizations that otherwise wouldn't be possible.
+ *
+ * Specifically copying and moving are much, much faster.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Filesystem extends DAV\Tree {
+
+ /**
+ * Base url on the filesystem.
+ *
+ * @var string
+ */
+ protected $basePath;
+
+ /**
+ * Creates this tree
+ *
+ * Supply the path you'd like to share.
+ *
+ * @param string $basePath
+ */
+ public function __construct($basePath) {
+
+ $this->basePath = $basePath;
+
+ }
+
+ /**
+ * Returns a new node for the given path
+ *
+ * @param string $path
+ * @return DAV\FS\Node
+ */
+ public function getNodeForPath($path) {
+
+ $realPath = $this->getRealPath($path);
+ if (!file_exists($realPath)) {
+ throw new DAV\Exception\NotFound('File at location ' . $realPath . ' not found');
+ }
+ if (is_dir($realPath)) {
+ return new DAV\FS\Directory($realPath);
+ } else {
+ return new DAV\FS\File($realPath);
+ }
+
+ }
+
+ /**
+ * Returns the real filesystem path for a webdav url.
+ *
+ * @param string $publicPath
+ * @return string
+ */
+ protected function getRealPath($publicPath) {
+
+ return rtrim($this->basePath,'/') . '/' . trim($publicPath,'/');
+
+ }
+
+ /**
+ * Copies a file or directory.
+ *
+ * This method must work recursively and delete the destination
+ * if it exists
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ */
+ public function copy($source,$destination) {
+
+ $source = $this->getRealPath($source);
+ $destination = $this->getRealPath($destination);
+ $this->realCopy($source,$destination);
+
+ }
+
+ /**
+ * Used by self::copy
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ */
+ protected function realCopy($source,$destination) {
+
+ if (is_file($source)) {
+ copy($source,$destination);
+ } else {
+ mkdir($destination);
+ foreach(scandir($source) as $subnode) {
+
+ if ($subnode=='.' || $subnode=='..') continue;
+ $this->realCopy($source.'/'.$subnode,$destination.'/'.$subnode);
+
+ }
+ }
+
+ }
+
+ /**
+ * Moves a file or directory recursively.
+ *
+ * If the destination exists, delete it first.
+ *
+ * @param string $source
+ * @param string $destination
+ * @return void
+ */
+ public function move($source,$destination) {
+
+ $source = $this->getRealPath($source);
+ $destination = $this->getRealPath($destination);
+ rename($source,$destination);
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php
new file mode 100644
index 0000000..f6ebb47
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/URLUtil.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * URL utility class
+ *
+ * This class provides methods to deal with encoding and decoding url (percent encoded) strings.
+ *
+ * It was not possible to use PHP's built-in methods for this, because some clients don't like
+ * encoding of certain characters.
+ *
+ * Specifically, it was found that GVFS (gnome's webdav client) does not like encoding of ( and
+ * ). Since these are reserved, but don't have a reserved meaning in url, these characters are
+ * kept as-is.
+ *
+ * It was also discovered that versions of the SOGO connector for thunderbird
+ * has issues with urlencoded colons.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class URLUtil {
+
+ /**
+ * Encodes the path of a url.
+ *
+ * slashes (/) are treated as path-separators.
+ *
+ * @param string $path
+ * @return string
+ */
+ static function encodePath($path) {
+
+ return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:])/',function($match) {
+
+ return '%'.sprintf('%02x',ord($match[0]));
+
+ }, $path);
+
+ }
+
+ /**
+ * Encodes a 1 segment of a path
+ *
+ * Slashes are considered part of the name, and are encoded as %2f
+ *
+ * @param string $pathSegment
+ * @return string
+ */
+ static function encodePathSegment($pathSegment) {
+
+ return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):])/',function($match) {
+
+ return '%'.sprintf('%02x',ord($match[0]));
+
+ }, $pathSegment);
+ }
+
+ /**
+ * Decodes a url-encoded path
+ *
+ * @param string $path
+ * @return string
+ */
+ static function decodePath($path) {
+
+ return self::decodePathSegment($path);
+
+ }
+
+ /**
+ * Decodes a url-encoded path segment
+ *
+ * @param string $path
+ * @return string
+ */
+ static function decodePathSegment($path) {
+
+ $path = rawurldecode($path);
+ $encoding = mb_detect_encoding($path, array('UTF-8','ISO-8859-1'));
+
+ switch($encoding) {
+
+ case 'ISO-8859-1' :
+ $path = utf8_encode($path);
+
+ }
+
+ return $path;
+
+ }
+
+ /**
+ * Returns the 'dirname' and 'basename' for a path.
+ *
+ * The reason there is a custom function for this purpose, is because
+ * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale is used)
+ * and we need a method that just operates on UTF-8 characters.
+ *
+ * In addition basename and dirname are platform aware, and will treat backslash (\) as a
+ * directory separator on windows.
+ *
+ * This method returns the 2 components as an array.
+ *
+ * If there is no dirname, it will return an empty string. Any / appearing at the end of the
+ * string is stripped off.
+ *
+ * @param string $path
+ * @return array
+ */
+ static function splitPath($path) {
+
+ $matches = array();
+ if(preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u',$path,$matches)) {
+ return array($matches[1],$matches[2]);
+ } else {
+ return array(null,null);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php
new file mode 100644
index 0000000..aed84e8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/UUIDUtil.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * UUID Utility
+ *
+ * This class has static methods to generate and validate UUID's.
+ * UUIDs are used a decent amount within various *DAV standards, so it made
+ * sense to include it.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class UUIDUtil {
+
+ /**
+ * Returns a pseudo-random v4 UUID
+ *
+ * This function is based on a comment by Andrew Moore on php.net
+ *
+ * @see http://www.php.net/manual/en/function.uniqid.php#94959
+ * @return string
+ */
+ static function getUUID() {
+
+ return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+ // 32 bits for "time_low"
+ mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
+
+ // 16 bits for "time_mid"
+ mt_rand( 0, 0xffff ),
+
+ // 16 bits for "time_hi_and_version",
+ // four most significant bits holds version number 4
+ mt_rand( 0, 0x0fff ) | 0x4000,
+
+ // 16 bits, 8 bits for "clk_seq_hi_res",
+ // 8 bits for "clk_seq_low",
+ // two most significant bits holds zero and one for variant DCE1.1
+ mt_rand( 0, 0x3fff ) | 0x8000,
+
+ // 48 bits for "node"
+ mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
+ );
+ }
+
+ /**
+ * Checks if a string is a valid UUID.
+ *
+ * @param string $uuid
+ * @return bool
+ */
+ static function validateUUID($uuid) {
+
+ return preg_match(
+ '/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i',
+ $uuid
+ ) == true;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php
new file mode 100644
index 0000000..62e35b3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * This class contains the SabreDAV version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.12';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php b/calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php
new file mode 100644
index 0000000..5ee0b5d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAV/XMLUtil.php
@@ -0,0 +1,191 @@
+<?php
+
+namespace OldSabre\DAV;
+
+/**
+ * XML utilities for WebDAV
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class XMLUtil {
+
+ /**
+ * Returns the 'clark notation' for an element.
+ *
+ * For example, and element encoded as:
+ * <b:myelem xmlns:b="http://www.example.org/" />
+ * will be returned as:
+ * {http://www.example.org}myelem
+ *
+ * This format is used throughout the SabreDAV sourcecode.
+ * Elements encoded with the urn:DAV namespace will
+ * be returned as if they were in the DAV: namespace. This is to avoid
+ * compatibility problems.
+ *
+ * This function will return null if a nodetype other than an Element is passed.
+ *
+ * @param \DOMNode $dom
+ * @return string
+ */
+ static function toClarkNotation(\DOMNode $dom) {
+
+ if ($dom->nodeType !== XML_ELEMENT_NODE) return null;
+
+ // Mapping back to the real namespace, in case it was dav
+ if ($dom->namespaceURI=='urn:DAV') $ns = 'DAV:'; else $ns = $dom->namespaceURI;
+
+ // Mapping to clark notation
+ return '{' . $ns . '}' . $dom->localName;
+
+ }
+
+ /**
+ * Parses a clark-notation string, and returns the namespace and element
+ * name components.
+ *
+ * If the string was invalid, it will throw an InvalidArgumentException.
+ *
+ * @param string $str
+ * @throws InvalidArgumentException
+ * @return array
+ */
+ static function parseClarkNotation($str) {
+
+ if (!preg_match('/^{([^}]*)}(.*)$/',$str,$matches)) {
+ throw new \InvalidArgumentException('\'' . $str . '\' is not a valid clark-notation formatted string');
+ }
+
+ return array(
+ $matches[1],
+ $matches[2]
+ );
+
+ }
+
+ /**
+ * This method takes an XML document (as string) and converts all instances of the
+ * DAV: namespace to urn:DAV
+ *
+ * This is unfortunately needed, because the DAV: namespace violates the xml namespaces
+ * spec, and causes the DOM to throw errors
+ *
+ * @param string $xmlDocument
+ * @return array|string|null
+ */
+ static function convertDAVNamespace($xmlDocument) {
+
+ // This is used to map the DAV: namespace to urn:DAV. This is needed, because the DAV:
+ // namespace is actually a violation of the XML namespaces specification, and will cause errors
+ return preg_replace("/xmlns(:[A-Za-z0-9_]*)?=(\"|\')DAV:(\\2)/","xmlns\\1=\\2urn:DAV\\2",$xmlDocument);
+
+ }
+
+ /**
+ * This method provides a generic way to load a DOMDocument for WebDAV use.
+ *
+ * This method throws a OldSabre\DAV\Exception\BadRequest exception for any xml errors.
+ * It does not preserve whitespace, and it converts the DAV: namespace to urn:DAV.
+ *
+ * @param string $xml
+ * @throws OldSabre\DAV\Exception\BadRequest
+ * @return DOMDocument
+ */
+ static function loadDOMDocument($xml) {
+
+ if (empty($xml))
+ throw new Exception\BadRequest('Empty XML document sent');
+
+ // The BitKinex client sends xml documents as UTF-16. PHP 5.3.1 (and presumably lower)
+ // does not support this, so we must intercept this and convert to UTF-8.
+ if (substr($xml,0,12) === "\x3c\x00\x3f\x00\x78\x00\x6d\x00\x6c\x00\x20\x00") {
+
+ // Note: the preceeding byte sequence is "<?xml" encoded as UTF_16, without the BOM.
+ $xml = iconv('UTF-16LE','UTF-8',$xml);
+
+ // Because the xml header might specify the encoding, we must also change this.
+ // This regex looks for the string encoding="UTF-16" and replaces it with
+ // encoding="UTF-8".
+ $xml = preg_replace('|<\?xml([^>]*)encoding="UTF-16"([^>]*)>|u','<?xml\1encoding="UTF-8"\2>',$xml);
+
+ }
+
+ // Retaining old error setting
+ $oldErrorSetting = libxml_use_internal_errors(true);
+ // Fixes an XXE vulnerability on PHP versions older than 5.3.23 or
+ // 5.4.13.
+ $oldEntityLoaderSetting = libxml_disable_entity_loader(true);
+
+ // Clearing any previous errors
+ libxml_clear_errors();
+
+ $dom = new \DOMDocument();
+
+ // We don't generally care about any whitespace
+ $dom->preserveWhiteSpace = false;
+
+ $dom->loadXML(self::convertDAVNamespace($xml),LIBXML_NOWARNING | LIBXML_NOERROR);
+
+ if ($error = libxml_get_last_error()) {
+ libxml_clear_errors();
+ throw new Exception\BadRequest('The request body had an invalid XML body. (message: ' . $error->message . ', errorcode: ' . $error->code . ', line: ' . $error->line . ')');
+ }
+
+ // Restoring old mechanism for error handling
+ if ($oldErrorSetting===false) libxml_use_internal_errors(false);
+ if ($oldEntityLoaderSetting===false) libxml_disable_entity_loader(false);
+
+ return $dom;
+
+ }
+
+ /**
+ * Parses all WebDAV properties out of a DOM Element
+ *
+ * Generally WebDAV properties are enclosed in {DAV:}prop elements. This
+ * method helps by going through all these and pulling out the actual
+ * propertynames, making them array keys and making the property values,
+ * well.. the array values.
+ *
+ * If no value was given (self-closing element) null will be used as the
+ * value. This is used in for example PROPFIND requests.
+ *
+ * Complex values are supported through the propertyMap argument. The
+ * propertyMap should have the clark-notation properties as it's keys, and
+ * classnames as values.
+ *
+ * When any of these properties are found, the unserialize() method will be
+ * (statically) called. The result of this method is used as the value.
+ *
+ * @param \DOMElement $parentNode
+ * @param array $propertyMap
+ * @return array
+ */
+ static function parseProperties(\DOMElement $parentNode, array $propertyMap = array()) {
+
+ $propList = array();
+ foreach($parentNode->childNodes as $propNode) {
+
+ if (self::toClarkNotation($propNode)!=='{DAV:}prop') continue;
+
+ foreach($propNode->childNodes as $propNodeData) {
+
+ /* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */
+ if ($propNodeData->nodeType != XML_ELEMENT_NODE) continue;
+
+ $propertyName = self::toClarkNotation($propNodeData);
+ if (isset($propertyMap[$propertyName])) {
+ $propList[$propertyName] = call_user_func(array($propertyMap[$propertyName],'unserialize'),$propNodeData);
+ } else {
+ $propList[$propertyName] = $propNodeData->textContent;
+ }
+ }
+
+
+ }
+ return $propList;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php
new file mode 100644
index 0000000..1367037
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/AbstractPrincipalCollection.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * Principals Collection
+ *
+ * This is a helper class that easily allows you to create a collection that
+ * has a childnode for every principal.
+ *
+ * To use this class, simply implement the getChildForPrincipal method.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractPrincipalCollection extends DAV\Collection implements IPrincipalCollection {
+
+ /**
+ * Node or 'directory' name.
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * Principal backend
+ *
+ * @var PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * If this value is set to true, it effectively disables listing of users
+ * it still allows user to find other users if they have an exact url.
+ *
+ * @var bool
+ */
+ public $disableListing = false;
+
+ /**
+ * Creates the object
+ *
+ * This object must be passed the principal backend. This object will
+ * filter all principals from a specified prefix ($principalPrefix). The
+ * default is 'principals', if your principals are stored in a different
+ * collection, override $principalPrefix
+ *
+ *
+ * @param PrincipalBackend\BackendInterface $principalBackend
+ * @param string $principalPrefix
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend, $principalPrefix = 'principals') {
+
+ $this->principalPrefix = $principalPrefix;
+ $this->principalBackend = $principalBackend;
+
+ }
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principalInfo
+ * @return IPrincipal
+ */
+ abstract function getChildForPrincipal(array $principalInfo);
+
+ /**
+ * Returns the name of this collection.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ list(,$name) = DAV\URLUtil::splitPath($this->principalPrefix);
+ return $name;
+
+ }
+
+ /**
+ * Return the list of users
+ *
+ * @return array
+ */
+ public function getChildren() {
+
+ if ($this->disableListing)
+ throw new DAV\Exception\MethodNotAllowed('Listing members of this collection is disabled');
+
+ $children = array();
+ foreach($this->principalBackend->getPrincipalsByPrefix($this->principalPrefix) as $principalInfo) {
+
+ $children[] = $this->getChildForPrincipal($principalInfo);
+
+
+ }
+ return $children;
+
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * @param string $name
+ * @throws DAV\Exception\NotFound
+ * @return IPrincipal
+ */
+ public function getChild($name) {
+
+ $principalInfo = $this->principalBackend->getPrincipalByPath($this->principalPrefix . '/' . $name);
+ if (!$principalInfo) throw new DAV\Exception\NotFound('Principal with name ' . $name . ' not found');
+ return $this->getChildForPrincipal($principalInfo);
+
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return a list of 'child names', which may be
+ * used to call $this->getChild in the future.
+ *
+ * @param array $searchProperties
+ * @return array
+ */
+ public function searchPrincipals(array $searchProperties) {
+
+ $result = $this->principalBackend->searchPrincipals($this->principalPrefix, $searchProperties);
+ $r = array();
+
+ foreach($result as $row) {
+ list(, $r[]) = DAV\URLUtil::splitPath($row);
+ }
+
+ return $r;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php
new file mode 100644
index 0000000..a636bdc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/AceConflict.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * This exception is thrown when a client attempts to set conflicting
+ * permissions.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AceConflict extends DAV\Exception\Conflict {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}no-ace-conflict element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:no-ace-conflict');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php
new file mode 100644
index 0000000..c2c3ab1
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NeedPrivileges.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * NeedPrivileges
+ *
+ * The 403-need privileges is thrown when a user didn't have the appropriate
+ * permissions to perform an operation
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NeedPrivileges extends DAV\Exception\Forbidden {
+
+ /**
+ * The relevant uri
+ *
+ * @var string
+ */
+ protected $uri;
+
+ /**
+ * The privileges the user didn't have.
+ *
+ * @var array
+ */
+ protected $privileges;
+
+ /**
+ * Constructor
+ *
+ * @param string $uri
+ * @param array $privileges
+ */
+ public function __construct($uri,array $privileges) {
+
+ $this->uri = $uri;
+ $this->privileges = $privileges;
+
+ parent::__construct('User did not have the required privileges (' . implode(',', $privileges) . ') for path "' . $uri . '"');
+
+ }
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}need-privileges element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:need-privileges');
+ $errorNode->appendChild($np);
+
+ foreach($this->privileges as $privilege) {
+
+ $resource = $doc->createElementNS('DAV:','d:resource');
+ $np->appendChild($resource);
+
+ $resource->appendChild($doc->createElementNS('DAV:','d:href',$server->getBaseUri() . $this->uri));
+
+ $priv = $doc->createElementNS('DAV:','d:privilege');
+ $resource->appendChild($priv);
+
+ preg_match('/^{([^}]*)}(.*)$/',$privilege,$privilegeParts);
+ $priv->appendChild($doc->createElementNS($privilegeParts[1],'d:' . $privilegeParts[2]));
+
+
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php
new file mode 100644
index 0000000..b7c78d9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NoAbstract.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * This exception is thrown when a user tries to set a privilege that's marked
+ * as abstract.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NoAbstract extends DAV\Exception\PreconditionFailed {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}no-abstract element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:no-abstract');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php
new file mode 100644
index 0000000..c7f99e8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotRecognizedPrincipal.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * If a client tried to set a privilege assigned to a non-existant principal,
+ * this exception will be thrown.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotRecognizedPrincipal extends DAV\Exception\PreconditionFailed {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}recognized-principal element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:recognized-principal');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php
new file mode 100644
index 0000000..7e3adb6
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Exception/NotSupportedPrivilege.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace OldSabre\DAVACL\Exception;
+
+use OldSabre\DAV;
+
+/**
+ * If a client tried to set a privilege that doesn't exist, this exception will
+ * be thrown.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class NotSupportedPrivilege extends DAV\Exception\PreconditionFailed {
+
+ /**
+ * Adds in extra information in the xml response.
+ *
+ * This method adds the {DAV:}not-supported-privilege element as defined in rfc3744
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $errorNode
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $errorNode) {
+
+ $doc = $errorNode->ownerDocument;
+
+ $np = $doc->createElementNS('DAV:','d:not-supported-privilege');
+ $errorNode->appendChild($np);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php
new file mode 100644
index 0000000..7e0508f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IACL.php
@@ -0,0 +1,74 @@
+<?php
+
+namespace OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * ACL-enabled node
+ *
+ * If you want to add WebDAV ACL to a node, you must implement this class
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IACL extends DAV\INode {
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getOwner();
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ function getGroup();
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ function getACL();
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's as an array argument.
+ *
+ * @param array $acl
+ * @return void
+ */
+ function setACL(array $acl);
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ function getSupportedPrivilegeSet();
+
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php
new file mode 100644
index 0000000..3e2c95a
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipal.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+use OldSabre\DAV;
+
+/**
+ * IPrincipal interface
+ *
+ * Implement this interface to define your own principals
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IPrincipal extends DAV\INode {
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ function getAlternateUriSet();
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ function getPrincipalUrl();
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ function getGroupMemberSet();
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ function getGroupMembership();
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $principals
+ * @return void
+ */
+ function setGroupMemberSet(array $principals);
+
+ /**
+ * Returns the displayname
+ *
+ * This should be a human readable name for the principal.
+ * If none is available, return the nodename.
+ *
+ * @return string
+ */
+ function getDisplayName();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php
new file mode 100644
index 0000000..13a6c53
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/IPrincipalCollection.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+use OldSabre\DAV;
+
+/**
+ * Principal Collection interface.
+ *
+ * Implement this interface to ensure that your principal collection can be
+ * searched using the principal-property-search REPORT.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IPrincipalCollection extends DAV\INode {
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return a list of 'child names', which may be
+ * used to call $this->getChild in the future.
+ *
+ * @param array $searchProperties
+ * @return array
+ */
+ function searchPrincipals(array $searchProperties);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php
new file mode 100644
index 0000000..f2cc020
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Plugin.php
@@ -0,0 +1,1402 @@
+<?php
+
+namespace OldSabre\DAVACL;
+use OldSabre\DAV;
+
+/**
+ * SabreDAV ACL Plugin
+ *
+ * This plugin provides functionality to enforce ACL permissions.
+ * ACL is defined in RFC3744.
+ *
+ * In addition it also provides support for the {DAV:}current-user-principal
+ * property, defined in RFC5397 and the {DAV:}expand-property report, as
+ * defined in RFC3253.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+ /**
+ * Recursion constants
+ *
+ * This only checks the base node
+ */
+ const R_PARENT = 1;
+
+ /**
+ * Recursion constants
+ *
+ * This checks every node in the tree
+ */
+ const R_RECURSIVE = 2;
+
+ /**
+ * Recursion constants
+ *
+ * This checks every parentnode in the tree, but not leaf-nodes.
+ */
+ const R_RECURSIVEPARENTS = 3;
+
+ /**
+ * Reference to server object.
+ *
+ * @var OldSabre\DAV\Server
+ */
+ protected $server;
+
+ /**
+ * List of urls containing principal collections.
+ * Modify this if your principals are located elsewhere.
+ *
+ * @var array
+ */
+ public $principalCollectionSet = array(
+ 'principals',
+ );
+
+ /**
+ * By default ACL is only enforced for nodes that have ACL support (the
+ * ones that implement IACL). For any other node, access is
+ * always granted.
+ *
+ * To override this behaviour you can turn this setting off. This is useful
+ * if you plan to fully support ACL in the entire tree.
+ *
+ * @var bool
+ */
+ public $allowAccessToNodesWithoutACL = true;
+
+ /**
+ * By default nodes that are inaccessible by the user, can still be seen
+ * in directory listings (PROPFIND on parent with Depth: 1)
+ *
+ * In certain cases it's desirable to hide inaccessible nodes. Setting this
+ * to true will cause these nodes to be hidden from directory listings.
+ *
+ * @var bool
+ */
+ public $hideNodesFromListings = false;
+
+ /**
+ * This string is prepended to the username of the currently logged in
+ * user. This allows the plugin to determine the principal path based on
+ * the username.
+ *
+ * @var string
+ */
+ public $defaultUsernamePath = 'principals';
+
+ /**
+ * This list of properties are the properties a client can search on using
+ * the {DAV:}principal-property-search report.
+ *
+ * The keys are the property names, values are descriptions.
+ *
+ * @var array
+ */
+ public $principalSearchPropertySet = array(
+ '{DAV:}displayname' => 'Display name',
+ '{http://sabredav.org/ns}email-address' => 'Email address',
+ );
+
+ /**
+ * Any principal uri's added here, will automatically be added to the list
+ * of ACL's. They will effectively receive {DAV:}all privileges, as a
+ * protected privilege.
+ *
+ * @var array
+ */
+ public $adminPrincipals = array();
+
+ /**
+ * Returns a list of features added by this plugin.
+ *
+ * This list is used in the response of a HTTP OPTIONS request.
+ *
+ * @return array
+ */
+ public function getFeatures() {
+
+ return array('access-control', 'calendarserver-principal-property-search');
+
+ }
+
+ /**
+ * Returns a list of available methods for a given url
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getMethods($uri) {
+
+ return array('ACL');
+
+ }
+
+ /**
+ * Returns a plugin name.
+ *
+ * Using this name other plugins will be able to access other plugins
+ * using OldSabre\DAV\Server::getPlugin
+ *
+ * @return string
+ */
+ public function getPluginName() {
+
+ return 'acl';
+
+ }
+
+ /**
+ * Returns a list of reports this plugin supports.
+ *
+ * This will be used in the {DAV:}supported-report-set property.
+ * Note that you still need to subscribe to the 'report' event to actually
+ * implement them
+ *
+ * @param string $uri
+ * @return array
+ */
+ public function getSupportedReportSet($uri) {
+
+ return array(
+ '{DAV:}expand-property',
+ '{DAV:}principal-property-search',
+ '{DAV:}principal-search-property-set',
+ );
+
+ }
+
+
+ /**
+ * Checks if the current user has the specified privilege(s).
+ *
+ * You can specify a single privilege, or a list of privileges.
+ * This method will throw an exception if the privilege is not available
+ * and return true otherwise.
+ *
+ * @param string $uri
+ * @param array|string $privileges
+ * @param int $recursion
+ * @param bool $throwExceptions if set to false, this method won't throw exceptions.
+ * @throws OldSabre\DAVACL\Exception\NeedPrivileges
+ * @return bool
+ */
+ public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) {
+
+ if (!is_array($privileges)) $privileges = array($privileges);
+
+ $acl = $this->getCurrentUserPrivilegeSet($uri);
+
+ if (is_null($acl)) {
+ if ($this->allowAccessToNodesWithoutACL) {
+ return true;
+ } else {
+ if ($throwExceptions)
+ throw new Exception\NeedPrivileges($uri,$privileges);
+ else
+ return false;
+
+ }
+ }
+
+ $failed = array();
+ foreach($privileges as $priv) {
+
+ if (!in_array($priv, $acl)) {
+ $failed[] = $priv;
+ }
+
+ }
+
+ if ($failed) {
+ if ($throwExceptions)
+ throw new Exception\NeedPrivileges($uri,$failed);
+ else
+ return false;
+ }
+ return true;
+
+ }
+
+ /**
+ * Returns the standard users' principal.
+ *
+ * This is one authorative principal url for the current user.
+ * This method will return null if the user wasn't logged in.
+ *
+ * @return string|null
+ */
+ public function getCurrentUserPrincipal() {
+
+ $authPlugin = $this->server->getPlugin('auth');
+ if (is_null($authPlugin)) return null;
+ /** @var $authPlugin OldSabre\DAV\Auth\Plugin */
+
+ $userName = $authPlugin->getCurrentUser();
+ if (!$userName) return null;
+
+ return $this->defaultUsernamePath . '/' . $userName;
+
+ }
+
+
+ /**
+ * Returns a list of principals that's associated to the current
+ * user, either directly or through group membership.
+ *
+ * @return array
+ */
+ public function getCurrentUserPrincipals() {
+
+ $currentUser = $this->getCurrentUserPrincipal();
+
+ if (is_null($currentUser)) return array();
+
+ return array_merge(
+ array($currentUser),
+ $this->getPrincipalMembership($currentUser)
+ );
+
+ }
+
+ /**
+ * This array holds a cache for all the principals that are associated with
+ * a single principal.
+ *
+ * @var array
+ */
+ protected $principalMembershipCache = array();
+
+
+ /**
+ * Returns all the principal groups the specified principal is a member of.
+ *
+ * @param string $principal
+ * @return array
+ */
+ public function getPrincipalMembership($mainPrincipal) {
+
+ // First check our cache
+ if (isset($this->principalMembershipCache[$mainPrincipal])) {
+ return $this->principalMembershipCache[$mainPrincipal];
+ }
+
+ $check = array($mainPrincipal);
+ $principals = array();
+
+ while(count($check)) {
+
+ $principal = array_shift($check);
+
+ $node = $this->server->tree->getNodeForPath($principal);
+ if ($node instanceof IPrincipal) {
+ foreach($node->getGroupMembership() as $groupMember) {
+
+ if (!in_array($groupMember, $principals)) {
+
+ $check[] = $groupMember;
+ $principals[] = $groupMember;
+
+ }
+
+ }
+
+ }
+
+ }
+
+ // Store the result in the cache
+ $this->principalMembershipCache[$mainPrincipal] = $principals;
+
+ return $principals;
+
+ }
+
+ /**
+ * Returns the supported privilege structure for this ACL plugin.
+ *
+ * See RFC3744 for more details. Currently we default on a simple,
+ * standard structure.
+ *
+ * You can either get the list of privileges by a uri (path) or by
+ * specifying a Node.
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ public function getSupportedPrivilegeSet($node) {
+
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ if ($node instanceof IACL) {
+ $result = $node->getSupportedPrivilegeSet();
+
+ if ($result)
+ return $result;
+ }
+
+ return self::getDefaultSupportedPrivilegeSet();
+
+ }
+
+ /**
+ * Returns a fairly standard set of privileges, which may be useful for
+ * other systems to use as a basis.
+ *
+ * @return array
+ */
+ static function getDefaultSupportedPrivilegeSet() {
+
+ return array(
+ 'privilege' => '{DAV:}all',
+ 'abstract' => true,
+ 'aggregates' => array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'aggregates' => array(
+ array(
+ 'privilege' => '{DAV:}read-acl',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}read-current-user-privilege-set',
+ 'abstract' => true,
+ ),
+ ),
+ ), // {DAV:}read
+ array(
+ 'privilege' => '{DAV:}write',
+ 'aggregates' => array(
+ array(
+ 'privilege' => '{DAV:}write-acl',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write-properties',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}write-content',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}bind',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}unbind',
+ 'abstract' => true,
+ ),
+ array(
+ 'privilege' => '{DAV:}unlock',
+ 'abstract' => true,
+ ),
+ ),
+ ), // {DAV:}write
+ ),
+ ); // {DAV:}all
+
+ }
+
+ /**
+ * Returns the supported privilege set as a flat list
+ *
+ * This is much easier to parse.
+ *
+ * The returned list will be index by privilege name.
+ * The value is a struct containing the following properties:
+ * - aggregates
+ * - abstract
+ * - concrete
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ final public function getFlatPrivilegeSet($node) {
+
+ $privs = $this->getSupportedPrivilegeSet($node);
+
+ $flat = array();
+ $this->getFPSTraverse($privs, null, $flat);
+
+ return $flat;
+
+ }
+
+ /**
+ * Traverses the privilege set tree for reordering
+ *
+ * This function is solely used by getFlatPrivilegeSet, and would have been
+ * a closure if it wasn't for the fact I need to support PHP 5.2.
+ *
+ * @param array $priv
+ * @param $concrete
+ * @param array $flat
+ * @return void
+ */
+ final private function getFPSTraverse($priv, $concrete, &$flat) {
+
+ $myPriv = array(
+ 'privilege' => $priv['privilege'],
+ 'abstract' => isset($priv['abstract']) && $priv['abstract'],
+ 'aggregates' => array(),
+ 'concrete' => isset($priv['abstract']) && $priv['abstract']?$concrete:$priv['privilege'],
+ );
+
+ if (isset($priv['aggregates']))
+ foreach($priv['aggregates'] as $subPriv) $myPriv['aggregates'][] = $subPriv['privilege'];
+
+ $flat[$priv['privilege']] = $myPriv;
+
+ if (isset($priv['aggregates'])) {
+
+ foreach($priv['aggregates'] as $subPriv) {
+
+ $this->getFPSTraverse($subPriv, $myPriv['concrete'], $flat);
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Returns the full ACL list.
+ *
+ * Either a uri or a DAV\INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ public function getACL($node) {
+
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+ if (!$node instanceof IACL) {
+ return null;
+ }
+ $acl = $node->getACL();
+ foreach($this->adminPrincipals as $adminPrincipal) {
+ $acl[] = array(
+ 'principal' => $adminPrincipal,
+ 'privilege' => '{DAV:}all',
+ 'protected' => true,
+ );
+ }
+ return $acl;
+
+ }
+
+ /**
+ * Returns a list of privileges the current user has
+ * on a particular node.
+ *
+ * Either a uri or a DAV\INode may be passed.
+ *
+ * null will be returned if the node doesn't support ACLs.
+ *
+ * @param string|DAV\INode $node
+ * @return array
+ */
+ public function getCurrentUserPrivilegeSet($node) {
+
+ if (is_string($node)) {
+ $node = $this->server->tree->getNodeForPath($node);
+ }
+
+ $acl = $this->getACL($node);
+
+ if (is_null($acl)) return null;
+
+ $principals = $this->getCurrentUserPrincipals();
+
+ $collected = array();
+
+ foreach($acl as $ace) {
+
+ $principal = $ace['principal'];
+
+ switch($principal) {
+
+ case '{DAV:}owner' :
+ $owner = $node->getOwner();
+ if ($owner && in_array($owner, $principals)) {
+ $collected[] = $ace;
+ }
+ break;
+
+
+ // 'all' matches for every user
+ case '{DAV:}all' :
+
+ // 'authenticated' matched for every user that's logged in.
+ // Since it's not possible to use ACL while not being logged
+ // in, this is also always true.
+ case '{DAV:}authenticated' :
+ $collected[] = $ace;
+ break;
+
+ // 'unauthenticated' can never occur either, so we simply
+ // ignore these.
+ case '{DAV:}unauthenticated' :
+ break;
+
+ default :
+ if (in_array($ace['principal'], $principals)) {
+ $collected[] = $ace;
+ }
+ break;
+
+ }
+
+
+ }
+
+ // Now we deduct all aggregated privileges.
+ $flat = $this->getFlatPrivilegeSet($node);
+
+ $collected2 = array();
+ while(count($collected)) {
+
+ $current = array_pop($collected);
+ $collected2[] = $current['privilege'];
+
+ foreach($flat[$current['privilege']]['aggregates'] as $subPriv) {
+ $collected2[] = $subPriv;
+ $collected[] = $flat[$subPriv];
+ }
+
+ }
+
+ return array_values(array_unique($collected2));
+
+ }
+
+ /**
+ * Principal property search
+ *
+ * This method can search for principals matching certain values in
+ * properties.
+ *
+ * This method will return a list of properties for the matched properties.
+ *
+ * @param array $searchProperties The properties to search on. This is a
+ * key-value list. The keys are property
+ * names, and the values the strings to
+ * match them on.
+ * @param array $requestedProperties This is the list of properties to
+ * return for every match.
+ * @param string $collectionUri The principal collection to search on.
+ * If this is ommitted, the standard
+ * principal collection-set will be used.
+ * @return array This method returns an array structure similar to
+ * OldSabre\DAV\Server::getPropertiesForPath. Returned
+ * properties are index by a HTTP status code.
+ *
+ */
+ public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null) {
+
+ if (!is_null($collectionUri)) {
+ $uris = array($collectionUri);
+ } else {
+ $uris = $this->principalCollectionSet;
+ }
+
+ $lookupResults = array();
+ foreach($uris as $uri) {
+
+ $principalCollection = $this->server->tree->getNodeForPath($uri);
+ if (!$principalCollection instanceof IPrincipalCollection) {
+ // Not a principal collection, we're simply going to ignore
+ // this.
+ continue;
+ }
+
+ $results = $principalCollection->searchPrincipals($searchProperties);
+ foreach($results as $result) {
+ $lookupResults[] = rtrim($uri,'/') . '/' . $result;
+ }
+
+ }
+
+ $matches = array();
+
+ foreach($lookupResults as $lookupResult) {
+
+ list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0);
+
+ }
+
+ return $matches;
+
+ }
+
+ /**
+ * Sets up the plugin
+ *
+ * This method is automatically called by the server class.
+ *
+ * @param DAV\Server $server
+ * @return void
+ */
+ public function initialize(DAV\Server $server) {
+
+ $this->server = $server;
+ $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
+
+ $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'),20);
+ $server->subscribeEvent('beforeBind', array($this,'beforeBind'),20);
+ $server->subscribeEvent('beforeUnbind', array($this,'beforeUnbind'),20);
+ $server->subscribeEvent('updateProperties',array($this,'updateProperties'));
+ $server->subscribeEvent('beforeUnlock', array($this,'beforeUnlock'),20);
+ $server->subscribeEvent('report',array($this,'report'));
+ $server->subscribeEvent('unknownMethod', array($this, 'unknownMethod'));
+
+ array_push($server->protectedProperties,
+ '{DAV:}alternate-URI-set',
+ '{DAV:}principal-URL',
+ '{DAV:}group-membership',
+ '{DAV:}principal-collection-set',
+ '{DAV:}current-user-principal',
+ '{DAV:}supported-privilege-set',
+ '{DAV:}current-user-privilege-set',
+ '{DAV:}acl',
+ '{DAV:}acl-restrictions',
+ '{DAV:}inherited-acl-set',
+ '{DAV:}owner',
+ '{DAV:}group'
+ );
+
+ // Automatically mapping nodes implementing IPrincipal to the
+ // {DAV:}principal resourcetype.
+ $server->resourceTypeMapping['OldSabre\\DAVACL\\IPrincipal'] = '{DAV:}principal';
+
+ // Mapping the group-member-set property to the HrefList property
+ // class.
+ $server->propertyMap['{DAV:}group-member-set'] = 'OldSabre\\DAV\\Property\\HrefList';
+
+ }
+
+
+ /* {{{ Event handlers */
+
+ /**
+ * Triggered before any method is handled
+ *
+ * @param string $method
+ * @param string $uri
+ * @return void
+ */
+ public function beforeMethod($method, $uri) {
+
+ $exists = $this->server->tree->nodeExists($uri);
+
+ // If the node doesn't exists, none of these checks apply
+ if (!$exists) return;
+
+ switch($method) {
+
+ case 'GET' :
+ case 'HEAD' :
+ case 'OPTIONS' :
+ // For these 3 we only need to know if the node is readable.
+ $this->checkPrivileges($uri,'{DAV:}read');
+ break;
+
+ case 'PUT' :
+ case 'LOCK' :
+ case 'UNLOCK' :
+ // This method requires the write-content priv if the node
+ // already exists, and bind on the parent if the node is being
+ // created.
+ // The bind privilege is handled in the beforeBind event.
+ $this->checkPrivileges($uri,'{DAV:}write-content');
+ break;
+
+
+ case 'PROPPATCH' :
+ $this->checkPrivileges($uri,'{DAV:}write-properties');
+ break;
+
+ case 'ACL' :
+ $this->checkPrivileges($uri,'{DAV:}write-acl');
+ break;
+
+ case 'COPY' :
+ case 'MOVE' :
+ // Copy requires read privileges on the entire source tree.
+ // If the target exists write-content normally needs to be
+ // checked, however, we're deleting the node beforehand and
+ // creating a new one after, so this is handled by the
+ // beforeUnbind event.
+ //
+ // The creation of the new node is handled by the beforeBind
+ // event.
+ //
+ // If MOVE is used beforeUnbind will also be used to check if
+ // the sourcenode can be deleted.
+ $this->checkPrivileges($uri,'{DAV:}read',self::R_RECURSIVE);
+
+ break;
+
+ }
+
+ }
+
+ /**
+ * Triggered before a new node is created.
+ *
+ * This allows us to check permissions for any operation that creates a
+ * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function beforeBind($uri) {
+
+ list($parentUri,$nodeName) = DAV\URLUtil::splitPath($uri);
+ $this->checkPrivileges($parentUri,'{DAV:}bind');
+
+ }
+
+ /**
+ * Triggered before a node is deleted
+ *
+ * This allows us to check permissions for any operation that will delete
+ * an existing node.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function beforeUnbind($uri) {
+
+ list($parentUri,$nodeName) = DAV\URLUtil::splitPath($uri);
+ $this->checkPrivileges($parentUri,'{DAV:}unbind',self::R_RECURSIVEPARENTS);
+
+ }
+
+ /**
+ * Triggered before a node is unlocked.
+ *
+ * @param string $uri
+ * @param DAV\Locks\LockInfo $lock
+ * @TODO: not yet implemented
+ * @return void
+ */
+ public function beforeUnlock($uri, DAV\Locks\LockInfo $lock) {
+
+
+ }
+
+ /**
+ * Triggered before properties are looked up in specific nodes.
+ *
+ * @param string $uri
+ * @param DAV\INode $node
+ * @param array $requestedProperties
+ * @param array $returnedProperties
+ * @TODO really should be broken into multiple methods, or even a class.
+ * @return bool
+ */
+ public function beforeGetProperties($uri, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
+
+ // Checking the read permission
+ if (!$this->checkPrivileges($uri,'{DAV:}read',self::R_PARENT,false)) {
+
+ // User is not allowed to read properties
+ if ($this->hideNodesFromListings) {
+ return false;
+ }
+
+ // Marking all requested properties as '403'.
+ foreach($requestedProperties as $key=>$requestedProperty) {
+ unset($requestedProperties[$key]);
+ $returnedProperties[403][$requestedProperty] = null;
+ }
+ return;
+
+ }
+
+ /* Adding principal properties */
+ if ($node instanceof IPrincipal) {
+
+ if (false !== ($index = array_search('{DAV:}alternate-URI-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}alternate-URI-set'] = new DAV\Property\HrefList($node->getAlternateUriSet());
+
+ }
+ if (false !== ($index = array_search('{DAV:}principal-URL', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}principal-URL'] = new DAV\Property\Href($node->getPrincipalUrl() . '/');
+
+ }
+ if (false !== ($index = array_search('{DAV:}group-member-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}group-member-set'] = new DAV\Property\HrefList($node->getGroupMemberSet());
+
+ }
+ if (false !== ($index = array_search('{DAV:}group-membership', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}group-membership'] = new DAV\Property\HrefList($node->getGroupMembership());
+
+ }
+
+ if (false !== ($index = array_search('{DAV:}displayname', $requestedProperties))) {
+
+ $returnedProperties[200]['{DAV:}displayname'] = $node->getDisplayName();
+
+ }
+
+ }
+ if (false !== ($index = array_search('{DAV:}principal-collection-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $val = $this->principalCollectionSet;
+ // Ensuring all collections end with a slash
+ foreach($val as $k=>$v) $val[$k] = $v . '/';
+ $returnedProperties[200]['{DAV:}principal-collection-set'] = new DAV\Property\HrefList($val);
+
+ }
+ if (false !== ($index = array_search('{DAV:}current-user-principal', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ if ($url = $this->getCurrentUserPrincipal()) {
+ $returnedProperties[200]['{DAV:}current-user-principal'] = new Property\Principal(Property\Principal::HREF, $url . '/');
+ } else {
+ $returnedProperties[200]['{DAV:}current-user-principal'] = new Property\Principal(Property\Principal::UNAUTHENTICATED);
+ }
+
+ }
+ if (false !== ($index = array_search('{DAV:}supported-privilege-set', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}supported-privilege-set'] = new Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node));
+
+ }
+ if (false !== ($index = array_search('{DAV:}current-user-privilege-set', $requestedProperties))) {
+
+ if (!$this->checkPrivileges($uri, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) {
+ $returnedProperties[403]['{DAV:}current-user-privilege-set'] = null;
+ unset($requestedProperties[$index]);
+ } else {
+ $val = $this->getCurrentUserPrivilegeSet($node);
+ if (!is_null($val)) {
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}current-user-privilege-set'] = new Property\CurrentUserPrivilegeSet($val);
+ }
+ }
+
+ }
+
+ /* The ACL property contains all the permissions */
+ if (false !== ($index = array_search('{DAV:}acl', $requestedProperties))) {
+
+ if (!$this->checkPrivileges($uri, '{DAV:}read-acl', self::R_PARENT, false)) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[403]['{DAV:}acl'] = null;
+
+ } else {
+
+ $acl = $this->getACL($node);
+ if (!is_null($acl)) {
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}acl'] = new Property\Acl($this->getACL($node));
+ }
+
+ }
+
+ }
+
+ /* The acl-restrictions property contains information on how privileges
+ * must behave.
+ */
+ if (false !== ($index = array_search('{DAV:}acl-restrictions', $requestedProperties))) {
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}acl-restrictions'] = new Property\AclRestrictions();
+ }
+
+ /* Adding ACL properties */
+ if ($node instanceof IACL) {
+
+ if (false !== ($index = array_search('{DAV:}owner', $requestedProperties))) {
+
+ unset($requestedProperties[$index]);
+ $returnedProperties[200]['{DAV:}owner'] = new DAV\Property\Href($node->getOwner() . '/');
+
+ }
+
+ }
+
+ }
+
+ /**
+ * This method intercepts PROPPATCH methods and make sure the
+ * group-member-set is updated correctly.
+ *
+ * @param array $propertyDelta
+ * @param array $result
+ * @param DAV\INode $node
+ * @return bool
+ */
+ public function updateProperties(&$propertyDelta, &$result, DAV\INode $node) {
+
+ if (!array_key_exists('{DAV:}group-member-set', $propertyDelta))
+ return;
+
+ if (is_null($propertyDelta['{DAV:}group-member-set'])) {
+ $memberSet = array();
+ } elseif ($propertyDelta['{DAV:}group-member-set'] instanceof DAV\Property\HrefList) {
+ $memberSet = array_map(
+ array($this->server,'calculateUri'),
+ $propertyDelta['{DAV:}group-member-set']->getHrefs()
+ );
+ } else {
+ throw new DAV\Exception('The group-member-set property MUST be an instance of OldSabre\DAV\Property\HrefList or null');
+ }
+
+ if (!($node instanceof IPrincipal)) {
+ $result[403]['{DAV:}group-member-set'] = null;
+ unset($propertyDelta['{DAV:}group-member-set']);
+
+ // Returning false will stop the updateProperties process
+ return false;
+ }
+
+ $node->setGroupMemberSet($memberSet);
+ // We must also clear our cache, just in case
+
+ $this->principalMembershipCache = array();
+
+ $result[200]['{DAV:}group-member-set'] = null;
+ unset($propertyDelta['{DAV:}group-member-set']);
+
+ }
+
+ /**
+ * This method handles HTTP REPORT requests
+ *
+ * @param string $reportName
+ * @param \DOMNode $dom
+ * @return bool
+ */
+ public function report($reportName, $dom) {
+
+ switch($reportName) {
+
+ case '{DAV:}principal-property-search' :
+ $this->principalPropertySearchReport($dom);
+ return false;
+ case '{DAV:}principal-search-property-set' :
+ $this->principalSearchPropertySetReport($dom);
+ return false;
+ case '{DAV:}expand-property' :
+ $this->expandPropertyReport($dom);
+ return false;
+
+ }
+
+ }
+
+ /**
+ * This event is triggered for any HTTP method that is not known by the
+ * webserver.
+ *
+ * @param string $method
+ * @param string $uri
+ * @return bool
+ */
+ public function unknownMethod($method, $uri) {
+
+ if ($method!=='ACL') return;
+
+ $this->httpACL($uri);
+ return false;
+
+ }
+
+ /**
+ * This method is responsible for handling the 'ACL' event.
+ *
+ * @param string $uri
+ * @return void
+ */
+ public function httpACL($uri) {
+
+ $body = $this->server->httpRequest->getBody(true);
+ $dom = DAV\XMLUtil::loadDOMDocument($body);
+
+ $newAcl =
+ Property\Acl::unserialize($dom->firstChild)
+ ->getPrivileges();
+
+ // Normalizing urls
+ foreach($newAcl as $k=>$newAce) {
+ $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']);
+ }
+
+ $node = $this->server->tree->getNodeForPath($uri);
+
+ if (!($node instanceof IACL)) {
+ throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method');
+ }
+
+ $oldAcl = $this->getACL($node);
+
+ $supportedPrivileges = $this->getFlatPrivilegeSet($node);
+
+ /* Checking if protected principals from the existing principal set are
+ not overwritten. */
+ foreach($oldAcl as $oldAce) {
+
+ if (!isset($oldAce['protected']) || !$oldAce['protected']) continue;
+
+ $found = false;
+ foreach($newAcl as $newAce) {
+ if (
+ $newAce['privilege'] === $oldAce['privilege'] &&
+ $newAce['principal'] === $oldAce['principal'] &&
+ $newAce['protected']
+ )
+ $found = true;
+ }
+
+ if (!$found)
+ throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request');
+
+ }
+
+ foreach($newAcl as $newAce) {
+
+ // Do we recognize the privilege
+ if (!isset($supportedPrivileges[$newAce['privilege']])) {
+ throw new Exception\NotSupportedPrivilege('The privilege you specified (' . $newAce['privilege'] . ') is not recognized by this server');
+ }
+
+ if ($supportedPrivileges[$newAce['privilege']]['abstract']) {
+ throw new Exception\NoAbstract('The privilege you specified (' . $newAce['privilege'] . ') is an abstract privilege');
+ }
+
+ // Looking up the principal
+ try {
+ $principal = $this->server->tree->getNodeForPath($newAce['principal']);
+ } catch (DAV\Exception\NotFound $e) {
+ throw new Exception\NotRecognizedPrincipal('The specified principal (' . $newAce['principal'] . ') does not exist');
+ }
+ if (!($principal instanceof IPrincipal)) {
+ throw new Exception\NotRecognizedPrincipal('The specified uri (' . $newAce['principal'] . ') is not a principal');
+ }
+
+ }
+ $node->setACL($newAcl);
+
+ }
+
+ /* }}} */
+
+ /* Reports {{{ */
+
+ /**
+ * The expand-property report is defined in RFC3253 section 3-8.
+ *
+ * This report is very similar to a standard PROPFIND. The difference is
+ * that it has the additional ability to look at properties containing a
+ * {DAV:}href element, follow that property and grab additional elements
+ * there.
+ *
+ * Other rfc's, such as ACL rely on this report, so it made sense to put
+ * it in this plugin.
+ *
+ * @param \DOMElement $dom
+ * @return void
+ */
+ protected function expandPropertyReport($dom) {
+
+ $requestedProperties = $this->parseExpandPropertyReportRequest($dom->firstChild->firstChild);
+ $depth = $this->server->getHTTPDepth(0);
+ $requestUri = $this->server->getRequestUri();
+
+ $result = $this->expandProperties($requestUri,$requestedProperties,$depth);
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $multiStatus = $dom->createElement('d:multistatus');
+ $dom->appendChild($multiStatus);
+
+ // Adding in default namespaces
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+
+ foreach($result as $response) {
+ $response->serialize($this->server, $multiStatus);
+ }
+
+ $xml = $dom->saveXML();
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->sendBody($xml);
+
+ }
+
+ /**
+ * This method is used by expandPropertyReport to parse
+ * out the entire HTTP request.
+ *
+ * @param \DOMElement $node
+ * @return array
+ */
+ protected function parseExpandPropertyReportRequest($node) {
+
+ $requestedProperties = array();
+ do {
+
+ if (DAV\XMLUtil::toClarkNotation($node)!=='{DAV:}property') continue;
+
+ if ($node->firstChild) {
+
+ $children = $this->parseExpandPropertyReportRequest($node->firstChild);
+
+ } else {
+
+ $children = array();
+
+ }
+
+ $namespace = $node->getAttribute('namespace');
+ if (!$namespace) $namespace = 'DAV:';
+
+ $propName = '{'.$namespace.'}' . $node->getAttribute('name');
+ $requestedProperties[$propName] = $children;
+
+ } while ($node = $node->nextSibling);
+
+ return $requestedProperties;
+
+ }
+
+ /**
+ * This method expands all the properties and returns
+ * a list with property values
+ *
+ * @param array $path
+ * @param array $requestedProperties the list of required properties
+ * @param int $depth
+ * @return array
+ */
+ protected function expandProperties($path, array $requestedProperties, $depth) {
+
+ $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth);
+
+ $result = array();
+
+ foreach($foundProperties as $node) {
+
+ foreach($requestedProperties as $propertyName=>$childRequestedProperties) {
+
+ // We're only traversing if sub-properties were requested
+ if(count($childRequestedProperties)===0) continue;
+
+ // We only have to do the expansion if the property was found
+ // and it contains an href element.
+ if (!array_key_exists($propertyName,$node[200])) continue;
+
+ if ($node[200][$propertyName] instanceof DAV\Property\IHref) {
+ $hrefs = array($node[200][$propertyName]->getHref());
+ } elseif ($node[200][$propertyName] instanceof DAV\Property\HrefList) {
+ $hrefs = $node[200][$propertyName]->getHrefs();
+ }
+
+ $childProps = array();
+ foreach($hrefs as $href) {
+ $childProps = array_merge($childProps, $this->expandProperties($href, $childRequestedProperties, 0));
+ }
+ $node[200][$propertyName] = new DAV\Property\ResponseList($childProps);
+
+ }
+ $result[] = new DAV\Property\Response($node['href'], $node);
+
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * principalSearchPropertySetReport
+ *
+ * This method responsible for handing the
+ * {DAV:}principal-search-property-set report. This report returns a list
+ * of properties the client may search on, using the
+ * {DAV:}principal-property-search report.
+ *
+ * @param \DOMDocument $dom
+ * @return void
+ */
+ protected function principalSearchPropertySetReport(\DOMDocument $dom) {
+
+ $httpDepth = $this->server->getHTTPDepth(0);
+ if ($httpDepth!==0) {
+ throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0');
+ }
+
+ if ($dom->firstChild->hasChildNodes())
+ throw new DAV\Exception\BadRequest('The principal-search-property-set report element is not allowed to have child elements');
+
+ $dom = new \DOMDocument('1.0','utf-8');
+ $dom->formatOutput = true;
+ $root = $dom->createElement('d:principal-search-property-set');
+ $dom->appendChild($root);
+ // Adding in default namespaces
+ foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
+
+ $root->setAttribute('xmlns:' . $prefix,$namespace);
+
+ }
+
+ $nsList = $this->server->xmlNamespaces;
+
+ foreach($this->principalSearchPropertySet as $propertyName=>$description) {
+
+ $psp = $dom->createElement('d:principal-search-property');
+ $root->appendChild($psp);
+
+ $prop = $dom->createElement('d:prop');
+ $psp->appendChild($prop);
+
+ $propName = null;
+ preg_match('/^{([^}]*)}(.*)$/',$propertyName,$propName);
+
+ $currentProperty = $dom->createElement($nsList[$propName[1]] . ':' . $propName[2]);
+ $prop->appendChild($currentProperty);
+
+ $descriptionElem = $dom->createElement('d:description');
+ $descriptionElem->setAttribute('xml:lang','en');
+ $descriptionElem->appendChild($dom->createTextNode($description));
+ $psp->appendChild($descriptionElem);
+
+
+ }
+
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->sendStatus(200);
+ $this->server->httpResponse->sendBody($dom->saveXML());
+
+ }
+
+ /**
+ * principalPropertySearchReport
+ *
+ * This method is responsible for handing the
+ * {DAV:}principal-property-search report. This report can be used for
+ * clients to search for groups of principals, based on the value of one
+ * or more properties.
+ *
+ * @param \DOMDocument $dom
+ * @return void
+ */
+ protected function principalPropertySearchReport(\DOMDocument $dom) {
+
+ list($searchProperties, $requestedProperties, $applyToPrincipalCollectionSet) = $this->parsePrincipalPropertySearchReportRequest($dom);
+
+ $uri = null;
+ if (!$applyToPrincipalCollectionSet) {
+ $uri = $this->server->getRequestUri();
+ }
+ $result = $this->principalSearch($searchProperties, $requestedProperties, $uri);
+
+ $prefer = $this->server->getHTTPPRefer();
+
+ $this->server->httpResponse->sendStatus(207);
+ $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
+ $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
+ $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
+
+ }
+
+ /**
+ * parsePrincipalPropertySearchReportRequest
+ *
+ * This method parses the request body from a
+ * {DAV:}principal-property-search report.
+ *
+ * This method returns an array with two elements:
+ * 1. an array with properties to search on, and their values
+ * 2. a list of propertyvalues that should be returned for the request.
+ *
+ * @param \DOMDocument $dom
+ * @return array
+ */
+ protected function parsePrincipalPropertySearchReportRequest($dom) {
+
+ $httpDepth = $this->server->getHTTPDepth(0);
+ if ($httpDepth!==0) {
+ throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0');
+ }
+
+ $searchProperties = array();
+
+ $applyToPrincipalCollectionSet = false;
+
+ // Parsing the search request
+ foreach($dom->firstChild->childNodes as $searchNode) {
+
+ if (DAV\XMLUtil::toClarkNotation($searchNode) == '{DAV:}apply-to-principal-collection-set') {
+ $applyToPrincipalCollectionSet = true;
+ }
+
+ if (DAV\XMLUtil::toClarkNotation($searchNode)!=='{DAV:}property-search')
+ continue;
+
+ $propertyName = null;
+ $propertyValue = null;
+
+ foreach($searchNode->childNodes as $childNode) {
+
+ switch(DAV\XMLUtil::toClarkNotation($childNode)) {
+
+ case '{DAV:}prop' :
+ $property = DAV\XMLUtil::parseProperties($searchNode);
+ reset($property);
+ $propertyName = key($property);
+ break;
+
+ case '{DAV:}match' :
+ $propertyValue = $childNode->textContent;
+ break;
+
+ }
+
+
+ }
+
+ if (is_null($propertyName) || is_null($propertyValue))
+ throw new DAV\Exception\BadRequest('Invalid search request. propertyname: ' . $propertyName . '. propertvvalue: ' . $propertyValue);
+
+ $searchProperties[$propertyName] = $propertyValue;
+
+ }
+
+ return array($searchProperties, array_keys(DAV\XMLUtil::parseProperties($dom->firstChild)), $applyToPrincipalCollectionSet);
+
+ }
+
+
+ /* }}} */
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php
new file mode 100644
index 0000000..a78733d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Principal.php
@@ -0,0 +1,281 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+use OldSabre\DAV;
+
+/**
+ * Principal class
+ *
+ * This class is a representation of a simple principal
+ *
+ * Many WebDAV specs require a user to show up in the directory
+ * structure.
+ *
+ * This principal also has basic ACL settings, only allowing the principal
+ * access it's own principal.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Principal extends DAV\Node implements IPrincipal, DAV\IProperties, IACL {
+
+ /**
+ * Struct with principal information.
+ *
+ * @var array
+ */
+ protected $principalProperties;
+
+ /**
+ * Principal backend
+ *
+ * @var PrincipalBackend\BackendInterface
+ */
+ protected $principalBackend;
+
+ /**
+ * Creates the principal object
+ *
+ * @param IPrincipalBackend $principalBackend
+ * @param array $principalProperties
+ */
+ public function __construct(PrincipalBackend\BackendInterface $principalBackend, array $principalProperties = array()) {
+
+ if (!isset($principalProperties['uri'])) {
+ throw new DAV\Exception('The principal properties must at least contain the \'uri\' key');
+ }
+ $this->principalBackend = $principalBackend;
+ $this->principalProperties = $principalProperties;
+
+ }
+
+ /**
+ * Returns the full principal url
+ *
+ * @return string
+ */
+ public function getPrincipalUrl() {
+
+ return $this->principalProperties['uri'];
+
+ }
+
+ /**
+ * Returns a list of alternative urls for a principal
+ *
+ * This can for example be an email address, or ldap url.
+ *
+ * @return array
+ */
+ public function getAlternateUriSet() {
+
+ $uris = array();
+ if (isset($this->principalProperties['{DAV:}alternate-URI-set'])) {
+
+ $uris = $this->principalProperties['{DAV:}alternate-URI-set'];
+
+ }
+
+ if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) {
+ $uris[] = 'mailto:' . $this->principalProperties['{http://sabredav.org/ns}email-address'];
+ }
+
+ return array_unique($uris);
+
+ }
+
+ /**
+ * Returns the list of group members
+ *
+ * If this principal is a group, this function should return
+ * all member principal uri's for the group.
+ *
+ * @return array
+ */
+ public function getGroupMemberSet() {
+
+ return $this->principalBackend->getGroupMemberSet($this->principalProperties['uri']);
+
+ }
+
+ /**
+ * Returns the list of groups this principal is member of
+ *
+ * If this principal is a member of a (list of) groups, this function
+ * should return a list of principal uri's for it's members.
+ *
+ * @return array
+ */
+ public function getGroupMembership() {
+
+ return $this->principalBackend->getGroupMemberShip($this->principalProperties['uri']);
+
+ }
+
+
+ /**
+ * Sets a list of group members
+ *
+ * If this principal is a group, this method sets all the group members.
+ * The list of members is always overwritten, never appended to.
+ *
+ * This method should throw an exception if the members could not be set.
+ *
+ * @param array $groupMembers
+ * @return void
+ */
+ public function setGroupMemberSet(array $groupMembers) {
+
+ $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers);
+
+ }
+
+
+ /**
+ * Returns this principals name.
+ *
+ * @return string
+ */
+ public function getName() {
+
+ $uri = $this->principalProperties['uri'];
+ list(, $name) = DAV\URLUtil::splitPath($uri);
+ return $name;
+
+ }
+
+ /**
+ * Returns the name of the user
+ *
+ * @return string
+ */
+ public function getDisplayName() {
+
+ if (isset($this->principalProperties['{DAV:}displayname'])) {
+ return $this->principalProperties['{DAV:}displayname'];
+ } else {
+ return $this->getName();
+ }
+
+ }
+
+ /**
+ * Returns a list of properties
+ *
+ * @param array $requestedProperties
+ * @return array
+ */
+ public function getProperties($requestedProperties) {
+
+ $newProperties = array();
+ foreach($requestedProperties as $propName) {
+
+ if (isset($this->principalProperties[$propName])) {
+ $newProperties[$propName] = $this->principalProperties[$propName];
+ }
+
+ }
+
+ return $newProperties;
+
+ }
+
+ /**
+ * Updates this principals properties.
+ *
+ * @param array $mutations
+ * @see OldSabre\DAV\IProperties::updateProperties
+ * @return bool|array
+ */
+ public function updateProperties($mutations) {
+
+ return $this->principalBackend->updatePrincipal($this->principalProperties['uri'], $mutations);
+
+ }
+
+ /**
+ * Returns the owner principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getOwner() {
+
+ return $this->principalProperties['uri'];
+
+
+ }
+
+ /**
+ * Returns a group principal
+ *
+ * This must be a url to a principal, or null if there's no owner
+ *
+ * @return string|null
+ */
+ public function getGroup() {
+
+ return null;
+
+ }
+
+ /**
+ * Returns a list of ACE's for this node.
+ *
+ * Each ACE has the following properties:
+ * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+ * currently the only supported privileges
+ * * 'principal', a url to the principal who owns the node
+ * * 'protected' (optional), indicating that this ACE is not allowed to
+ * be updated.
+ *
+ * @return array
+ */
+ public function getACL() {
+
+ return array(
+ array(
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getPrincipalUrl(),
+ 'protected' => true,
+ ),
+ );
+
+ }
+
+ /**
+ * Updates the ACL
+ *
+ * This method will receive a list of new ACE's.
+ *
+ * @param array $acl
+ * @return void
+ */
+ public function setACL(array $acl) {
+
+ throw new DAV\Exception\MethodNotAllowed('Updating ACLs is not allowed here');
+
+ }
+
+ /**
+ * Returns the list of supported privileges for this node.
+ *
+ * The returned data structure is a list of nested privileges.
+ * See OldSabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+ * standard structure.
+ *
+ * If null is returned from this method, the default privilege set is used,
+ * which is fine for most common usecases.
+ *
+ * @return array|null
+ */
+ public function getSupportedPrivilegeSet() {
+
+ return null;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php
new file mode 100644
index 0000000..1be74c7
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/AbstractBackend.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace OldSabre\DAVACL\PrincipalBackend;
+
+/**
+ * Abstract Principal Backend
+ *
+ * Currently this class has no function. It's here for consistency and so we
+ * have a non-bc-breaking way to add a default generic implementation to
+ * functions we may add in the future.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php
new file mode 100644
index 0000000..52a00b3
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/BackendInterface.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace OldSabre\DAVACL\PrincipalBackend;
+
+/**
+ * Implement this interface to create your own principal backends.
+ *
+ * Creating backends for principals is entirely optional. You can also
+ * implement OldSabre\DAVACL\IPrincipal directly. This interface is used solely by
+ * OldSabre\DAVACL\AbstractPrincipalCollection.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actually injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ * @return array
+ */
+ function getPrincipalsByPrefix($prefixPath);
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @return array
+ */
+ function getPrincipalByPath($path);
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * The list of mutations is supplied as an array. Each key in the array is
+ * a propertyname, such as {DAV:}displayname.
+ *
+ * Each value is the actual value to be updated. If a value is null, it
+ * must be deleted.
+ *
+ * This method should be atomic. It must either completely succeed, or
+ * completely fail. Success and failure can simply be returned as 'true' or
+ * 'false'.
+ *
+ * It is also possible to return detailed failure information. In that case
+ * an array such as this should be returned:
+ *
+ * array(
+ * 200 => array(
+ * '{DAV:}prop1' => null,
+ * ),
+ * 201 => array(
+ * '{DAV:}prop2' => null,
+ * ),
+ * 403 => array(
+ * '{DAV:}prop3' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}prop4' => null,
+ * ),
+ * );
+ *
+ * In this previous example prop1 was successfully updated or deleted, and
+ * prop2 was succesfully created.
+ *
+ * prop3 failed to update due to '403 Forbidden' and because of this prop4
+ * also could not be updated with '424 Failed dependency'.
+ *
+ * This last example was actually incorrect. While 200 and 201 could appear
+ * in 1 response, if there's any error (403) the other properties should
+ * always fail with 423 (failed dependency).
+ *
+ * But anyway, if you don't want to scratch your head over this, just
+ * return true or false.
+ *
+ * @param string $path
+ * @param array $mutations
+ * @return array|bool
+ */
+ function updatePrincipal($path, $mutations);
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @return array
+ */
+ function searchPrincipals($prefixPath, array $searchProperties);
+
+ /**
+ * Returns the list of members for a group-principal
+ *
+ * @param string $principal
+ * @return array
+ */
+ function getGroupMemberSet($principal);
+
+ /**
+ * Returns the list of groups a principal is a member of
+ *
+ * @param string $principal
+ * @return array
+ */
+ function getGroupMembership($principal);
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ * @return void
+ */
+ function setGroupMemberSet($principal, array $members);
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php
new file mode 100644
index 0000000..92e8417
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalBackend/PDO.php
@@ -0,0 +1,428 @@
+<?php
+
+namespace OldSabre\DAVACL\PrincipalBackend;
+
+use OldSabre\DAV;
+use OldSabre\DAVACL;
+
+/**
+ * PDO principal backend
+ *
+ *
+ * This backend assumes all principals are in a single collection. The default collection
+ * is 'principals/', but this can be overriden.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend {
+
+ /**
+ * pdo
+ *
+ * @var PDO
+ */
+ protected $pdo;
+
+ /**
+ * PDO table name for 'principals'
+ *
+ * @var string
+ */
+ protected $tableName;
+
+ /**
+ * PDO table name for 'group members'
+ *
+ * @var string
+ */
+ protected $groupMembersTableName;
+
+ /**
+ * A list of additional fields to support
+ *
+ * @var array
+ */
+ protected $fieldMap = array(
+
+ /**
+ * This property can be used to display the users' real name.
+ */
+ '{DAV:}displayname' => array(
+ 'dbField' => 'displayname',
+ ),
+
+ /**
+ * This property is actually used by the CardDAV plugin, where it gets
+ * mapped to {http://calendarserver.orgi/ns/}me-card.
+ *
+ * The reason we don't straight-up use that property, is because
+ * me-card is defined as a property on the users' addressbook
+ * collection.
+ */
+ '{http://sabredav.org/ns}vcard-url' => array(
+ 'dbField' => 'vcardurl',
+ ),
+ /**
+ * This is the users' primary email-address.
+ */
+ '{http://sabredav.org/ns}email-address' => array(
+ 'dbField' => 'email',
+ ),
+ );
+
+ /**
+ * Sets up the backend.
+ *
+ * @param PDO $pdo
+ * @param string $tableName
+ * @param string $groupMembersTableName
+ */
+ public function __construct(\PDO $pdo, $tableName = 'principals', $groupMembersTableName = 'groupmembers') {
+
+ $this->pdo = $pdo;
+ $this->tableName = $tableName;
+ $this->groupMembersTableName = $groupMembersTableName;
+
+ }
+
+
+ /**
+ * Returns a list of principals based on a prefix.
+ *
+ * This prefix will often contain something like 'principals'. You are only
+ * expected to return principals that are in this base path.
+ *
+ * You are expected to return at least a 'uri' for every user, you can
+ * return any additional properties if you wish so. Common properties are:
+ * {DAV:}displayname
+ * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
+ * field that's actualy injected in a number of other properties. If
+ * you have an email address, use this property.
+ *
+ * @param string $prefixPath
+ * @return array
+ */
+ public function getPrincipalsByPrefix($prefixPath) {
+
+ $fields = array(
+ 'uri',
+ );
+
+ foreach($this->fieldMap as $key=>$value) {
+ $fields[] = $value['dbField'];
+ }
+ $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '. $this->tableName);
+
+ $principals = array();
+
+ while($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = DAV\URLUtil::splitPath($row['uri']);
+ if ($rowPrefix !== $prefixPath) continue;
+
+ $principal = array(
+ 'uri' => $row['uri'],
+ );
+ foreach($this->fieldMap as $key=>$value) {
+ if ($row[$value['dbField']]) {
+ $principal[$key] = $row[$value['dbField']];
+ }
+ }
+ $principals[] = $principal;
+
+ }
+
+ return $principals;
+
+ }
+
+ /**
+ * Returns a specific principal, specified by it's path.
+ * The returned structure should be the exact same as from
+ * getPrincipalsByPrefix.
+ *
+ * @param string $path
+ * @return array
+ */
+ public function getPrincipalByPath($path) {
+
+ $fields = array(
+ 'id',
+ 'uri',
+ );
+
+ foreach($this->fieldMap as $key=>$value) {
+ $fields[] = $value['dbField'];
+ }
+ $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '. $this->tableName . ' WHERE uri = ?');
+ $stmt->execute(array($path));
+
+ $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+ if (!$row) return;
+
+ $principal = array(
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ );
+ foreach($this->fieldMap as $key=>$value) {
+ if ($row[$value['dbField']]) {
+ $principal[$key] = $row[$value['dbField']];
+ }
+ }
+ return $principal;
+
+ }
+
+ /**
+ * Updates one ore more webdav properties on a principal.
+ *
+ * The list of mutations is supplied as an array. Each key in the array is
+ * a propertyname, such as {DAV:}displayname.
+ *
+ * Each value is the actual value to be updated. If a value is null, it
+ * must be deleted.
+ *
+ * This method should be atomic. It must either completely succeed, or
+ * completely fail. Success and failure can simply be returned as 'true' or
+ * 'false'.
+ *
+ * It is also possible to return detailed failure information. In that case
+ * an array such as this should be returned:
+ *
+ * array(
+ * 200 => array(
+ * '{DAV:}prop1' => null,
+ * ),
+ * 201 => array(
+ * '{DAV:}prop2' => null,
+ * ),
+ * 403 => array(
+ * '{DAV:}prop3' => null,
+ * ),
+ * 424 => array(
+ * '{DAV:}prop4' => null,
+ * ),
+ * );
+ *
+ * In this previous example prop1 was successfully updated or deleted, and
+ * prop2 was succesfully created.
+ *
+ * prop3 failed to update due to '403 Forbidden' and because of this prop4
+ * also could not be updated with '424 Failed dependency'.
+ *
+ * This last example was actually incorrect. While 200 and 201 could appear
+ * in 1 response, if there's any error (403) the other properties should
+ * always fail with 423 (failed dependency).
+ *
+ * But anyway, if you don't want to scratch your head over this, just
+ * return true or false.
+ *
+ * @param string $path
+ * @param array $mutations
+ * @return array|bool
+ */
+ public function updatePrincipal($path, $mutations) {
+
+ $updateAble = array();
+ foreach($mutations as $key=>$value) {
+
+ // We are not aware of this field, we must fail.
+ if (!isset($this->fieldMap[$key])) {
+
+ $response = array(
+ 403 => array(
+ $key => null,
+ ),
+ 424 => array(),
+ );
+
+ // Adding the rest to the response as a 424
+ foreach($mutations as $subKey=>$subValue) {
+ if ($subKey !== $key) {
+ $response[424][$subKey] = null;
+ }
+ }
+ return $response;
+ }
+
+ $updateAble[$this->fieldMap[$key]['dbField']] = $value;
+
+ }
+
+ // No fields to update
+ $query = "UPDATE " . $this->tableName . " SET ";
+
+ $first = true;
+ foreach($updateAble as $key => $value) {
+ if (!$first) {
+ $query.= ', ';
+ }
+ $first = false;
+ $query.= "$key = :$key ";
+ }
+ $query.='WHERE uri = :uri';
+ $stmt = $this->pdo->prepare($query);
+ $updateAble['uri'] = $path;
+ $stmt->execute($updateAble);
+
+ return true;
+
+ }
+
+ /**
+ * This method is used to search for principals matching a set of
+ * properties.
+ *
+ * This search is specifically used by RFC3744's principal-property-search
+ * REPORT. You should at least allow searching on
+ * http://sabredav.org/ns}email-address.
+ *
+ * The actual search should be a unicode-non-case-sensitive search. The
+ * keys in searchProperties are the WebDAV property names, while the values
+ * are the property values to search on.
+ *
+ * If multiple properties are being searched on, the search should be
+ * AND'ed.
+ *
+ * This method should simply return an array with full principal uri's.
+ *
+ * If somebody attempted to search on a property the backend does not
+ * support, you should simply return 0 results.
+ *
+ * You can also just return 0 results if you choose to not support
+ * searching at all, but keep in mind that this may stop certain features
+ * from working.
+ *
+ * @param string $prefixPath
+ * @param array $searchProperties
+ * @return array
+ */
+ public function searchPrincipals($prefixPath, array $searchProperties) {
+
+ $query = 'SELECT uri FROM ' . $this->tableName . ' WHERE 1=1 ';
+ $values = array();
+ foreach($searchProperties as $property => $value) {
+
+ switch($property) {
+
+ case '{DAV:}displayname' :
+ $query.=' AND displayname LIKE ?';
+ $values[] = '%' . $value . '%';
+ break;
+ case '{http://sabredav.org/ns}email-address' :
+ $query.=' AND email LIKE ?';
+ $values[] = '%' . $value . '%';
+ break;
+ default :
+ // Unsupported property
+ return array();
+
+ }
+
+ }
+ $stmt = $this->pdo->prepare($query);
+ $stmt->execute($values);
+
+ $principals = array();
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+ // Checking if the principal is in the prefix
+ list($rowPrefix) = DAV\URLUtil::splitPath($row['uri']);
+ if ($rowPrefix !== $prefixPath) continue;
+
+ $principals[] = $row['uri'];
+
+ }
+
+ return $principals;
+
+ }
+
+ /**
+ * Returns the list of members for a group-principal
+ *
+ * @param string $principal
+ * @return array
+ */
+ public function getGroupMemberSet($principal) {
+
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) throw new DAV\Exception('Principal not found');
+
+ $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
+ $stmt->execute(array($principal['id']));
+
+ $result = array();
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row['uri'];
+ }
+ return $result;
+
+ }
+
+ /**
+ * Returns the list of groups a principal is a member of
+ *
+ * @param string $principal
+ * @return array
+ */
+ public function getGroupMembership($principal) {
+
+ $principal = $this->getPrincipalByPath($principal);
+ if (!$principal) throw new DAV\Exception('Principal not found');
+
+ $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
+ $stmt->execute(array($principal['id']));
+
+ $result = array();
+ while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ $result[] = $row['uri'];
+ }
+ return $result;
+
+ }
+
+ /**
+ * Updates the list of group members for a group principal.
+ *
+ * The principals should be passed as a list of uri's.
+ *
+ * @param string $principal
+ * @param array $members
+ * @return void
+ */
+ public function setGroupMemberSet($principal, array $members) {
+
+ // Grabbing the list of principal id's.
+ $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? ' . str_repeat(', ? ', count($members)) . ');');
+ $stmt->execute(array_merge(array($principal), $members));
+
+ $memberIds = array();
+ $principalId = null;
+
+ while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+ if ($row['uri'] == $principal) {
+ $principalId = $row['id'];
+ } else {
+ $memberIds[] = $row['id'];
+ }
+ }
+ if (!$principalId) throw new DAV\Exception('Principal not found');
+
+ // Wiping out old members
+ $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
+ $stmt->execute(array($principalId));
+
+ foreach($memberIds as $memberId) {
+
+ $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
+ $stmt->execute(array($principalId, $memberId));
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php
new file mode 100644
index 0000000..7cee8dc
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/PrincipalCollection.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+/**
+ * Principals Collection
+ *
+ * This collection represents a list of users.
+ * The users are instances of OldSabre\DAVACL\Principal
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PrincipalCollection extends AbstractPrincipalCollection {
+
+ /**
+ * This method returns a node for a principal.
+ *
+ * The passed array contains principal information, and is guaranteed to
+ * at least contain a uri item. Other properties may or may not be
+ * supplied by the authentication backend.
+ *
+ * @param array $principal
+ * @return \OldSabre\DAV\INode
+ */
+ public function getChildForPrincipal(array $principal) {
+
+ return new Principal($this->principalBackend, $principal);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php
new file mode 100644
index 0000000..33f5c2e
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Acl.php
@@ -0,0 +1,211 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * This class represents the {DAV:}acl property
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Acl extends DAV\Property {
+
+ /**
+ * List of privileges
+ *
+ * @var array
+ */
+ private $privileges;
+
+ /**
+ * Whether or not the server base url is required to be prefixed when
+ * serializing the property.
+ *
+ * @var boolean
+ */
+ private $prefixBaseUrl;
+
+ /**
+ * Constructor
+ *
+ * This object requires a structure similar to the return value from
+ * OldSabre\DAVACL\Plugin::getACL().
+ *
+ * Each privilege is a an array with at least a 'privilege' property, and a
+ * 'principal' property. A privilege may have a 'protected' property as
+ * well.
+ *
+ * The prefixBaseUrl should be set to false, if the supplied principal urls
+ * are already full urls. If this is kept to true, the servers base url
+ * will automatically be prefixed.
+ *
+ * @param bool $prefixBaseUrl
+ * @param array $privileges
+ */
+ public function __construct(array $privileges, $prefixBaseUrl = true) {
+
+ $this->privileges = $privileges;
+ $this->prefixBaseUrl = $prefixBaseUrl;
+
+ }
+
+ /**
+ * Returns the list of privileges for this property
+ *
+ * @return array
+ */
+ public function getPrivileges() {
+
+ return $this->privileges;
+
+ }
+
+ /**
+ * Serializes the property into a DOMElement
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ foreach($this->privileges as $ace) {
+
+ $this->serializeAce($doc, $node, $ace, $server);
+
+ }
+
+ }
+
+ /**
+ * Unserializes the {DAV:}acl xml element.
+ *
+ * @param \DOMElement $dom
+ * @return Acl
+ */
+ static public function unserialize(\DOMElement $dom) {
+
+ $privileges = array();
+ $xaces = $dom->getElementsByTagNameNS('urn:DAV','ace');
+ for($ii=0; $ii < $xaces->length; $ii++) {
+
+ $xace = $xaces->item($ii);
+ $principal = $xace->getElementsByTagNameNS('urn:DAV','principal');
+ if ($principal->length !== 1) {
+ throw new DAV\Exception\BadRequest('Each {DAV:}ace element must have one {DAV:}principal element');
+ }
+ $principal = Principal::unserialize($principal->item(0));
+
+ switch($principal->getType()) {
+ case Principal::HREF :
+ $principal = $principal->getHref();
+ break;
+ case Principal::AUTHENTICATED :
+ $principal = '{DAV:}authenticated';
+ break;
+ case Principal::UNAUTHENTICATED :
+ $principal = '{DAV:}unauthenticated';
+ break;
+ case Principal::ALL :
+ $principal = '{DAV:}all';
+ break;
+
+ }
+
+ $protected = false;
+
+ if ($xace->getElementsByTagNameNS('urn:DAV','protected')->length > 0) {
+ $protected = true;
+ }
+
+ $grants = $xace->getElementsByTagNameNS('urn:DAV','grant');
+ if ($grants->length < 1) {
+ throw new DAV\Exception\NotImplemented('Every {DAV:}ace element must have a {DAV:}grant element. {DAV:}deny is not yet supported');
+ }
+ $grant = $grants->item(0);
+
+ $xprivs = $grant->getElementsByTagNameNS('urn:DAV','privilege');
+ for($jj=0; $jj<$xprivs->length; $jj++) {
+
+ $xpriv = $xprivs->item($jj);
+
+ $privilegeName = null;
+
+ for ($kk=0;$kk<$xpriv->childNodes->length;$kk++) {
+
+ $childNode = $xpriv->childNodes->item($kk);
+ if ($t = DAV\XMLUtil::toClarkNotation($childNode)) {
+ $privilegeName = $t;
+ break;
+ }
+ }
+ if (is_null($privilegeName)) {
+ throw new DAV\Exception\BadRequest('{DAV:}privilege elements must have a privilege element contained within them.');
+ }
+
+ $privileges[] = array(
+ 'principal' => $principal,
+ 'protected' => $protected,
+ 'privilege' => $privilegeName,
+ );
+
+ }
+
+ }
+
+ return new self($privileges);
+
+ }
+
+ /**
+ * Serializes a single access control entry.
+ *
+ * @param \DOMDocument $doc
+ * @param \DOMElement $node
+ * @param array $ace
+ * @param DAV\Server $server
+ * @return void
+ */
+ private function serializeAce($doc,$node,$ace, DAV\Server $server) {
+
+ $xace = $doc->createElementNS('DAV:','d:ace');
+ $node->appendChild($xace);
+
+ $principal = $doc->createElementNS('DAV:','d:principal');
+ $xace->appendChild($principal);
+ switch($ace['principal']) {
+ case '{DAV:}authenticated' :
+ $principal->appendChild($doc->createElementNS('DAV:','d:authenticated'));
+ break;
+ case '{DAV:}unauthenticated' :
+ $principal->appendChild($doc->createElementNS('DAV:','d:unauthenticated'));
+ break;
+ case '{DAV:}all' :
+ $principal->appendChild($doc->createElementNS('DAV:','d:all'));
+ break;
+ default:
+ $principal->appendChild($doc->createElementNS('DAV:','d:href',($this->prefixBaseUrl?$server->getBaseUri():'') . $ace['principal'] . '/'));
+ }
+
+ $grant = $doc->createElementNS('DAV:','d:grant');
+ $xace->appendChild($grant);
+
+ $privParts = null;
+
+ preg_match('/^{([^}]*)}(.*)$/',$ace['privilege'],$privParts);
+
+ $xprivilege = $doc->createElementNS('DAV:','d:privilege');
+ $grant->appendChild($xprivilege);
+
+ $xprivilege->appendChild($doc->createElementNS($privParts[1],'d:'.$privParts[2]));
+
+ if (isset($ace['protected']) && $ace['protected'])
+ $xace->appendChild($doc->createElement('d:protected'));
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php
new file mode 100644
index 0000000..f9485ef
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/AclRestrictions.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * AclRestrictions property
+ *
+ * This property represents {DAV:}acl-restrictions, as defined in RFC3744.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AclRestrictions extends DAV\Property {
+
+ /**
+ * Serializes the property into a DOMElement
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $elem
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $elem) {
+
+ $doc = $elem->ownerDocument;
+
+ $elem->appendChild($doc->createElementNS('DAV:','d:grant-only'));
+ $elem->appendChild($doc->createElementNS('DAV:','d:no-invert'));
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php
new file mode 100644
index 0000000..e364121
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/CurrentUserPrivilegeSet.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * CurrentUserPrivilegeSet
+ *
+ * This class represents the current-user-privilege-set property. When
+ * requested, it contain all the privileges a user has on a specific node.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CurrentUserPrivilegeSet extends DAV\Property {
+
+ /**
+ * List of privileges
+ *
+ * @var array
+ */
+ private $privileges;
+
+ /**
+ * Creates the object
+ *
+ * Pass the privileges in clark-notation
+ *
+ * @param array $privileges
+ */
+ public function __construct(array $privileges) {
+
+ $this->privileges = $privileges;
+
+ }
+
+ /**
+ * Serializes the property in the DOM
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ foreach($this->privileges as $privName) {
+
+ $this->serializePriv($doc,$node,$privName);
+
+ }
+
+ }
+
+ /**
+ * Returns true or false, whether the specified principal appears in the
+ * list.
+ *
+ * @return bool
+ */
+ public function has($privilegeName) {
+
+ return in_array($privilegeName, $this->privileges);
+
+ }
+
+ /**
+ * Serializes one privilege
+ *
+ * @param \DOMDocument $doc
+ * @param \DOMElement $node
+ * @param string $privName
+ * @return void
+ */
+ protected function serializePriv($doc,$node,$privName) {
+
+ $xp = $doc->createElementNS('DAV:','d:privilege');
+ $node->appendChild($xp);
+
+ $privParts = null;
+ preg_match('/^{([^}]*)}(.*)$/',$privName,$privParts);
+
+ $xp->appendChild($doc->createElementNS($privParts[1],'d:'.$privParts[2]));
+
+ }
+
+ /**
+ * Unserializes the {DAV:}current-user-privilege-set element.
+ *
+ * @param DOMElement $node
+ * @return CurrentUserPrivilegeSet
+ */
+ static public function unserialize(\DOMElement $node) {
+
+ $result = array();
+
+ $xprivs = $node->getElementsByTagNameNS('urn:DAV','privilege');
+
+ for($jj=0; $jj<$xprivs->length; $jj++) {
+
+ $xpriv = $xprivs->item($jj);
+
+ $privilegeName = null;
+
+ for ($kk=0;$kk<$xpriv->childNodes->length;$kk++) {
+
+ $childNode = $xpriv->childNodes->item($kk);
+ if ($t = DAV\XMLUtil::toClarkNotation($childNode)) {
+ $privilegeName = $t;
+ break;
+ }
+ }
+
+ $result[] = $privilegeName;
+
+ }
+
+ return new self($result);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php
new file mode 100644
index 0000000..fbf7c1f
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/Principal.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+use OldSabre\DAV;
+
+/**
+ * Principal property
+ *
+ * The principal property represents a principal from RFC3744 (ACL).
+ * The property can be used to specify a principal or pseudo principals.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Principal extends DAV\Property implements DAV\Property\IHref {
+
+ /**
+ * To specify a not-logged-in user, use the UNAUTHENTICATED principal
+ */
+ const UNAUTHENTICATED = 1;
+
+ /**
+ * To specify any principal that is logged in, use AUTHENTICATED
+ */
+ const AUTHENTICATED = 2;
+
+ /**
+ * Specific principals can be specified with the HREF
+ */
+ const HREF = 3;
+
+ /**
+ * Everybody, basically
+ */
+ const ALL = 4;
+
+ /**
+ * Principal-type
+ *
+ * Must be one of the UNAUTHENTICATED, AUTHENTICATED or HREF constants.
+ *
+ * @var int
+ */
+ private $type;
+
+ /**
+ * Url to principal
+ *
+ * This value is only used for the HREF principal type.
+ *
+ * @var string
+ */
+ private $href;
+
+ /**
+ * Creates the property.
+ *
+ * The 'type' argument must be one of the type constants defined in this class.
+ *
+ * 'href' is only required for the HREF type.
+ *
+ * @param int $type
+ * @param string|null $href
+ */
+ public function __construct($type, $href = null) {
+
+ $this->type = $type;
+
+ if ($type===self::HREF && is_null($href)) {
+ throw new DAV\Exception('The href argument must be specified for the HREF principal type.');
+ }
+ $this->href = $href;
+
+ }
+
+ /**
+ * Returns the principal type
+ *
+ * @return int
+ */
+ public function getType() {
+
+ return $this->type;
+
+ }
+
+ /**
+ * Returns the principal uri.
+ *
+ * @return string
+ */
+ public function getHref() {
+
+ return $this->href;
+
+ }
+
+ /**
+ * Serializes the property into a DOMElement.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server, \DOMElement $node) {
+
+ $prefix = $server->xmlNamespaces['DAV:'];
+ switch($this->type) {
+
+ case self::UNAUTHENTICATED :
+ $node->appendChild(
+ $node->ownerDocument->createElement($prefix . ':unauthenticated')
+ );
+ break;
+ case self::AUTHENTICATED :
+ $node->appendChild(
+ $node->ownerDocument->createElement($prefix . ':authenticated')
+ );
+ break;
+ case self::HREF :
+ $href = $node->ownerDocument->createElement($prefix . ':href');
+ $href->nodeValue = $server->getBaseUri() . DAV\URLUtil::encodePath($this->href);
+ $node->appendChild($href);
+ break;
+
+ }
+
+ }
+
+ /**
+ * Deserializes a DOM element into a property object.
+ *
+ * @param \DOMElement $dom
+ * @return Principal
+ */
+ static public function unserialize(\DOMElement $dom) {
+
+ $parent = $dom->firstChild;
+ while(!DAV\XMLUtil::toClarkNotation($parent)) {
+ $parent = $parent->nextSibling;
+ }
+
+ switch(DAV\XMLUtil::toClarkNotation($parent)) {
+
+ case '{DAV:}unauthenticated' :
+ return new self(self::UNAUTHENTICATED);
+ case '{DAV:}authenticated' :
+ return new self(self::AUTHENTICATED);
+ case '{DAV:}href':
+ return new self(self::HREF, $parent->textContent);
+ case '{DAV:}all':
+ return new self(self::ALL);
+ default :
+ throw new DAV\Exception\BadRequest('Unexpected element (' . DAV\XMLUtil::toClarkNotation($parent) . '). Could not deserialize');
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php
new file mode 100644
index 0000000..30d1d06
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Property/SupportedPrivilegeSet.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace OldSabre\DAVACL\Property;
+
+use OldSabre\DAV;
+
+/**
+ * SupportedPrivilegeSet property
+ *
+ * This property encodes the {DAV:}supported-privilege-set property, as defined
+ * in rfc3744. Please consult the rfc for details about it's structure.
+ *
+ * This class expects a structure like the one given from
+ * OldSabre\DAVACL\Plugin::getSupportedPrivilegeSet as the argument in its
+ * constructor.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class SupportedPrivilegeSet extends DAV\Property {
+
+ /**
+ * privileges
+ *
+ * @var array
+ */
+ private $privileges;
+
+ /**
+ * Constructor
+ *
+ * @param array $privileges
+ */
+ public function __construct(array $privileges) {
+
+ $this->privileges = $privileges;
+
+ }
+
+ /**
+ * Serializes the property into a domdocument.
+ *
+ * @param DAV\Server $server
+ * @param \DOMElement $node
+ * @return void
+ */
+ public function serialize(DAV\Server $server,\DOMElement $node) {
+
+ $doc = $node->ownerDocument;
+ $this->serializePriv($doc, $node, $this->privileges);
+
+ }
+
+ /**
+ * Serializes a property
+ *
+ * This is a recursive function.
+ *
+ * @param \DOMDocument $doc
+ * @param \DOMElement $node
+ * @param array $privilege
+ * @return void
+ */
+ private function serializePriv($doc,$node,$privilege) {
+
+ $xsp = $doc->createElementNS('DAV:','d:supported-privilege');
+ $node->appendChild($xsp);
+
+ $xp = $doc->createElementNS('DAV:','d:privilege');
+ $xsp->appendChild($xp);
+
+ $privParts = null;
+ preg_match('/^{([^}]*)}(.*)$/',$privilege['privilege'],$privParts);
+
+ $xp->appendChild($doc->createElementNS($privParts[1],'d:'.$privParts[2]));
+
+ if (isset($privilege['abstract']) && $privilege['abstract']) {
+ $xsp->appendChild($doc->createElementNS('DAV:','d:abstract'));
+ }
+
+ if (isset($privilege['description'])) {
+ $xsp->appendChild($doc->createElementNS('DAV:','d:description',$privilege['description']));
+ }
+
+ if (isset($privilege['aggregates'])) {
+ foreach($privilege['aggregates'] as $subPrivilege) {
+ $this->serializePriv($doc,$xsp,$subPrivilege);
+ }
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php
new file mode 100644
index 0000000..e674cf2
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/DAVACL/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\DAVACL;
+
+/**
+ * This class contains the SabreDAV version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.7';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php
new file mode 100644
index 0000000..f080983
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AWSAuth.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP AWS Authentication handler
+ *
+ * Use this class to leverage amazon's AWS authentication header
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class AWSAuth extends AbstractAuth {
+
+ /**
+ * The signature supplied by the HTTP client
+ *
+ * @var string
+ */
+ private $signature = null;
+
+ /**
+ * The accesskey supplied by the HTTP client
+ *
+ * @var string
+ */
+ private $accessKey = null;
+
+ /**
+ * An error code, if any
+ *
+ * This value will be filled with one of the ERR_* constants
+ *
+ * @var int
+ */
+ public $errorCode = 0;
+
+ const ERR_NOAWSHEADER = 1;
+ const ERR_MD5CHECKSUMWRONG = 2;
+ const ERR_INVALIDDATEFORMAT = 3;
+ const ERR_REQUESTTIMESKEWED = 4;
+ const ERR_INVALIDSIGNATURE = 5;
+
+ /**
+ * Gathers all information from the headers
+ *
+ * This method needs to be called prior to anything else.
+ *
+ * @return bool
+ */
+ public function init() {
+
+ $authHeader = $this->httpRequest->getHeader('Authorization');
+ $authHeader = explode(' ',$authHeader);
+
+ if ($authHeader[0]!='AWS' || !isset($authHeader[1])) {
+ $this->errorCode = self::ERR_NOAWSHEADER;
+ return false;
+ }
+
+ list($this->accessKey,$this->signature) = explode(':',$authHeader[1]);
+
+ return true;
+
+ }
+
+ /**
+ * Returns the username for the request
+ *
+ * @return string
+ */
+ public function getAccessKey() {
+
+ return $this->accessKey;
+
+ }
+
+ /**
+ * Validates the signature based on the secretKey
+ *
+ * @param string $secretKey
+ * @return bool
+ */
+ public function validate($secretKey) {
+
+ $contentMD5 = $this->httpRequest->getHeader('Content-MD5');
+
+ if ($contentMD5) {
+ // We need to validate the integrity of the request
+ $body = $this->httpRequest->getBody(true);
+ $this->httpRequest->setBody($body,true);
+
+ if ($contentMD5!=base64_encode(md5($body,true))) {
+ // content-md5 header did not match md5 signature of body
+ $this->errorCode = self::ERR_MD5CHECKSUMWRONG;
+ return false;
+ }
+
+ }
+
+ if (!$requestDate = $this->httpRequest->getHeader('x-amz-date'))
+ $requestDate = $this->httpRequest->getHeader('Date');
+
+ if (!$this->validateRFC2616Date($requestDate))
+ return false;
+
+ $amzHeaders = $this->getAmzHeaders();
+
+ $signature = base64_encode(
+ $this->hmacsha1($secretKey,
+ $this->httpRequest->getMethod() . "\n" .
+ $contentMD5 . "\n" .
+ $this->httpRequest->getHeader('Content-type') . "\n" .
+ $requestDate . "\n" .
+ $amzHeaders .
+ $this->httpRequest->getURI()
+ )
+ );
+
+ if ($this->signature != $signature) {
+
+ $this->errorCode = self::ERR_INVALIDSIGNATURE;
+ return false;
+
+ }
+
+ return true;
+
+ }
+
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ public function requireLogin() {
+
+ $this->httpResponse->setHeader('WWW-Authenticate','AWS');
+ $this->httpResponse->sendStatus(401);
+
+ }
+
+ /**
+ * Makes sure the supplied value is a valid RFC2616 date.
+ *
+ * If we would just use strtotime to get a valid timestamp, we have no way of checking if a
+ * user just supplied the word 'now' for the date header.
+ *
+ * This function also makes sure the Date header is within 15 minutes of the operating
+ * system date, to prevent replay attacks.
+ *
+ * @param string $dateHeader
+ * @return bool
+ */
+ protected function validateRFC2616Date($dateHeader) {
+
+ $date = Util::parseHTTPDate($dateHeader);
+
+ // Unknown format
+ if (!$date) {
+ $this->errorCode = self::ERR_INVALIDDATEFORMAT;
+ return false;
+ }
+
+ $min = new \DateTime('-15 minutes');
+ $max = new \DateTime('+15 minutes');
+
+ // We allow 15 minutes around the current date/time
+ if ($date > $max || $date < $min) {
+ $this->errorCode = self::ERR_REQUESTTIMESKEWED;
+ return false;
+ }
+
+ return $date;
+
+ }
+
+ /**
+ * Returns a list of AMZ headers
+ *
+ * @return string
+ */
+ protected function getAmzHeaders() {
+
+ $amzHeaders = array();
+ $headers = $this->httpRequest->getHeaders();
+ foreach($headers as $headerName => $headerValue) {
+ if (strpos(strtolower($headerName),'x-amz-')===0) {
+ $amzHeaders[strtolower($headerName)] = str_replace(array("\r\n"),array(' '),$headerValue) . "\n";
+ }
+ }
+ ksort($amzHeaders);
+
+ $headerStr = '';
+ foreach($amzHeaders as $h=>$v) {
+ $headerStr.=$h.':'.$v;
+ }
+
+ return $headerStr;
+
+ }
+
+ /**
+ * Generates an HMAC-SHA1 signature
+ *
+ * @param string $key
+ * @param string $message
+ * @return string
+ */
+ private function hmacsha1($key, $message) {
+
+ $blocksize=64;
+ if (strlen($key)>$blocksize)
+ $key=pack('H*', sha1($key));
+ $key=str_pad($key,$blocksize,chr(0x00));
+ $ipad=str_repeat(chr(0x36),$blocksize);
+ $opad=str_repeat(chr(0x5c),$blocksize);
+ $hmac = pack('H*',sha1(($key^$opad).pack('H*',sha1(($key^$ipad).$message))));
+ return $hmac;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php
new file mode 100644
index 0000000..584088d
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/AbstractAuth.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Authentication baseclass
+ *
+ * This class has the common functionality for BasicAuth and DigestAuth
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractAuth {
+
+ /**
+ * The realm will be displayed in the dialog boxes
+ *
+ * This identifier can be changed through setRealm()
+ *
+ * @var string
+ */
+ protected $realm = 'SabreDAV';
+
+ /**
+ * HTTP response helper
+ *
+ * @var OldSabre\HTTP\Response
+ */
+ protected $httpResponse;
+
+
+ /**
+ * HTTP request helper
+ *
+ * @var OldSabre\HTTP\Request
+ */
+ protected $httpRequest;
+
+ /**
+ * __construct
+ *
+ */
+ public function __construct() {
+
+ $this->httpResponse = new Response();
+ $this->httpRequest = new Request();
+
+ }
+
+ /**
+ * Sets an alternative HTTP response object
+ *
+ * @param Response $response
+ * @return void
+ */
+ public function setHTTPResponse(Response $response) {
+
+ $this->httpResponse = $response;
+
+ }
+
+ /**
+ * Sets an alternative HTTP request object
+ *
+ * @param Request $request
+ * @return void
+ */
+ public function setHTTPRequest(Request $request) {
+
+ $this->httpRequest = $request;
+
+ }
+
+
+ /**
+ * Sets the realm
+ *
+ * The realm is often displayed in authentication dialog boxes
+ * Commonly an application name displayed here
+ *
+ * @param string $realm
+ * @return void
+ */
+ public function setRealm($realm) {
+
+ $this->realm = $realm;
+
+ }
+
+ /**
+ * Returns the realm
+ *
+ * @return string
+ */
+ public function getRealm() {
+
+ return $this->realm;
+
+ }
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ abstract public function requireLogin();
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php
new file mode 100644
index 0000000..e970fb4
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/BasicAuth.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Basic Authentication handler
+ *
+ * Use this class for easy http authentication setup
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class BasicAuth extends AbstractAuth {
+
+ /**
+ * Returns the supplied username and password.
+ *
+ * The returned array has two values:
+ * * 0 - username
+ * * 1 - password
+ *
+ * If nothing was supplied, 'false' will be returned
+ *
+ * @return mixed
+ */
+ public function getUserPass() {
+
+ // Apache and mod_php
+ if (($user = $this->httpRequest->getRawServerValue('PHP_AUTH_USER'))!==null && ($pass = $this->httpRequest->getRawServerValue('PHP_AUTH_PW'))!==null) {
+
+ return array($user,$pass);
+
+ }
+
+ // Most other webservers
+ $auth = $this->httpRequest->getHeader('Authorization');
+
+ // Apache could prefix environment variables with REDIRECT_ when urls
+ // are passed through mod_rewrite
+ if (!$auth) {
+ $auth = $this->httpRequest->getRawServerValue('REDIRECT_HTTP_AUTHORIZATION');
+ }
+
+ if (!$auth) return false;
+
+ if (strpos(strtolower($auth),'basic')!==0) return false;
+
+ return explode(':', base64_decode(substr($auth, 6)),2);
+
+ }
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ public function requireLogin() {
+
+ $this->httpResponse->setHeader('WWW-Authenticate','Basic realm="' . $this->realm . '"');
+ $this->httpResponse->sendStatus(401);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php
new file mode 100644
index 0000000..d21a548
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/DigestAuth.php
@@ -0,0 +1,240 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Digest Authentication handler
+ *
+ * Use this class for easy http digest authentication.
+ * Instructions:
+ *
+ * 1. Create the object
+ * 2. Call the setRealm() method with the realm you plan to use
+ * 3. Call the init method function.
+ * 4. Call the getUserName() function. This function may return false if no
+ * authentication information was supplied. Based on the username you
+ * should check your internal database for either the associated password,
+ * or the so-called A1 hash of the digest.
+ * 5. Call either validatePassword() or validateA1(). This will return true
+ * or false.
+ * 6. To make sure an authentication prompt is displayed, call the
+ * requireLogin() method.
+ *
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class DigestAuth extends AbstractAuth {
+
+ /**
+ * These constants are used in setQOP();
+ */
+ const QOP_AUTH = 1;
+ const QOP_AUTHINT = 2;
+
+ protected $nonce;
+ protected $opaque;
+ protected $digestParts;
+ protected $A1;
+ protected $qop = self::QOP_AUTH;
+
+ /**
+ * Initializes the object
+ */
+ public function __construct() {
+
+ $this->nonce = uniqid();
+ $this->opaque = md5($this->realm);
+ parent::__construct();
+
+ }
+
+ /**
+ * Gathers all information from the headers
+ *
+ * This method needs to be called prior to anything else.
+ *
+ * @return void
+ */
+ public function init() {
+
+ $digest = $this->getDigest();
+ $this->digestParts = $this->parseDigest($digest);
+
+ }
+
+ /**
+ * Sets the quality of protection value.
+ *
+ * Possible values are:
+ * OldSabre\HTTP\DigestAuth::QOP_AUTH
+ * OldSabre\HTTP\DigestAuth::QOP_AUTHINT
+ *
+ * Multiple values can be specified using logical OR.
+ *
+ * QOP_AUTHINT ensures integrity of the request body, but this is not
+ * supported by most HTTP clients. QOP_AUTHINT also requires the entire
+ * request body to be md5'ed, which can put strains on CPU and memory.
+ *
+ * @param int $qop
+ * @return void
+ */
+ public function setQOP($qop) {
+
+ $this->qop = $qop;
+
+ }
+
+ /**
+ * Validates the user.
+ *
+ * The A1 parameter should be md5($username . ':' . $realm . ':' . $password);
+ *
+ * @param string $A1
+ * @return bool
+ */
+ public function validateA1($A1) {
+
+ $this->A1 = $A1;
+ return $this->validate();
+
+ }
+
+ /**
+ * Validates authentication through a password. The actual password must be provided here.
+ * It is strongly recommended not store the password in plain-text and use validateA1 instead.
+ *
+ * @param string $password
+ * @return bool
+ */
+ public function validatePassword($password) {
+
+ $this->A1 = md5($this->digestParts['username'] . ':' . $this->realm . ':' . $password);
+ return $this->validate();
+
+ }
+
+ /**
+ * Returns the username for the request
+ *
+ * @return string
+ */
+ public function getUsername() {
+
+ return $this->digestParts['username'];
+
+ }
+
+ /**
+ * Validates the digest challenge
+ *
+ * @return bool
+ */
+ protected function validate() {
+
+ $A2 = $this->httpRequest->getMethod() . ':' . $this->digestParts['uri'];
+
+ if ($this->digestParts['qop']=='auth-int') {
+ // Making sure we support this qop value
+ if (!($this->qop & self::QOP_AUTHINT)) return false;
+ // We need to add an md5 of the entire request body to the A2 part of the hash
+ $body = $this->httpRequest->getBody(true);
+ $this->httpRequest->setBody($body,true);
+ $A2 .= ':' . md5($body);
+ } else {
+
+ // We need to make sure we support this qop value
+ if (!($this->qop & self::QOP_AUTH)) return false;
+ }
+
+ $A2 = md5($A2);
+
+ $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}");
+
+ return $this->digestParts['response']==$validResponse;
+
+
+ }
+
+ /**
+ * Returns an HTTP 401 header, forcing login
+ *
+ * This should be called when username and password are incorrect, or not supplied at all
+ *
+ * @return void
+ */
+ public function requireLogin() {
+
+ $qop = '';
+ switch($this->qop) {
+ case self::QOP_AUTH : $qop = 'auth'; break;
+ case self::QOP_AUTHINT : $qop = 'auth-int'; break;
+ case self::QOP_AUTH | self::QOP_AUTHINT : $qop = 'auth,auth-int'; break;
+ }
+
+ $this->httpResponse->setHeader('WWW-Authenticate','Digest realm="' . $this->realm . '",qop="'.$qop.'",nonce="' . $this->nonce . '",opaque="' . $this->opaque . '"');
+ $this->httpResponse->sendStatus(401);
+
+ }
+
+
+ /**
+ * This method returns the full digest string.
+ *
+ * It should be compatibile with mod_php format and other webservers.
+ *
+ * If the header could not be found, null will be returned
+ *
+ * @return mixed
+ */
+ public function getDigest() {
+
+ // mod_php
+ $digest = $this->httpRequest->getRawServerValue('PHP_AUTH_DIGEST');
+ if ($digest) return $digest;
+
+ // most other servers
+ $digest = $this->httpRequest->getHeader('Authorization');
+
+ // Apache could prefix environment variables with REDIRECT_ when urls
+ // are passed through mod_rewrite
+ if (!$digest) {
+ $digest = $this->httpRequest->getRawServerValue('REDIRECT_HTTP_AUTHORIZATION');
+ }
+
+ if ($digest && strpos(strtolower($digest),'digest')===0) {
+ return substr($digest,7);
+ } else {
+ return null;
+ }
+
+ }
+
+
+ /**
+ * Parses the different pieces of the digest string into an array.
+ *
+ * This method returns false if an incomplete digest was supplied
+ *
+ * @param string $digest
+ * @return mixed
+ */
+ protected function parseDigest($digest) {
+
+ // protect against missing data
+ $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1);
+ $data = array();
+
+ preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER);
+
+ foreach ($matches as $m) {
+ $data[$m[1]] = $m[2] ? $m[2] : $m[3];
+ unset($needed_parts[$m[1]]);
+ }
+
+ return $needed_parts ? false : $data;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php
new file mode 100644
index 0000000..d4cad65
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Request.php
@@ -0,0 +1,284 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP Request information
+ *
+ * This object can be used to easily access information about an HTTP request.
+ * It can additionally be used to create 'mock' requests.
+ *
+ * This class mostly operates independent, but because of the nature of a single
+ * request per run it can operate as a singleton. For more information check out
+ * the behaviour around 'defaultInputStream'.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Request {
+
+ /**
+ * PHP's $_SERVER data
+ *
+ * @var array
+ */
+ protected $_SERVER;
+
+ /**
+ * PHP's $_POST data
+ *
+ * @var array
+ */
+ protected $_POST;
+
+ /**
+ * The request body, if any.
+ *
+ * This is stored in the form of a stream resource.
+ *
+ * @var resource
+ */
+ protected $body = null;
+
+ /**
+ * This will be set as the 'default' inputStream for a specific HTTP request
+ * We sometimes need to retain, or rebuild this if we need multiple runs
+ * of parsing the original HTTP request.
+ *
+ * @var resource
+ */
+ static $defaultInputStream=null;
+
+ /**
+ * Sets up the object
+ *
+ * The serverData and postData array can be used to override usage of PHP's
+ * global _SERVER and _POST variable respectively.
+ *
+ * @param array $serverData
+ * @param array $postData
+ */
+ public function __construct(array $serverData = null, array $postData = null) {
+
+ if ($serverData) $this->_SERVER = $serverData;
+ else $this->_SERVER =& $_SERVER;
+
+ if ($postData) $this->_POST = $postData;
+ else $this->_POST =& $_POST;
+
+ }
+
+ /**
+ * Returns the value for a specific http header.
+ *
+ * This method returns null if the header did not exist.
+ *
+ * @param string $name
+ * @return string
+ */
+ public function getHeader($name) {
+
+ $name = strtoupper(str_replace(array('-'),array('_'),$name));
+ if (isset($this->_SERVER['HTTP_' . $name])) {
+ return $this->_SERVER['HTTP_' . $name];
+ }
+
+ // There's a few headers that seem to end up in the top-level
+ // server array.
+ switch($name) {
+ case 'CONTENT_TYPE' :
+ case 'CONTENT_LENGTH' :
+ if (isset($this->_SERVER[$name])) {
+ return $this->_SERVER[$name];
+ }
+ break;
+
+ }
+ return;
+
+ }
+
+ /**
+ * Returns all (known) HTTP headers.
+ *
+ * All headers are converted to lower-case, and additionally all underscores
+ * are automatically converted to dashes
+ *
+ * @return array
+ */
+ public function getHeaders() {
+
+ $hdrs = array();
+ foreach($this->_SERVER as $key=>$value) {
+
+ switch($key) {
+ case 'CONTENT_LENGTH' :
+ case 'CONTENT_TYPE' :
+ $hdrs[strtolower(str_replace('_','-',$key))] = $value;
+ break;
+ default :
+ if (strpos($key,'HTTP_')===0) {
+ $hdrs[substr(strtolower(str_replace('_','-',$key)),5)] = $value;
+ }
+ break;
+ }
+
+ }
+
+ return $hdrs;
+
+ }
+
+ /**
+ * Returns the HTTP request method
+ *
+ * This is for example POST or GET
+ *
+ * @return string
+ */
+ public function getMethod() {
+
+ return $this->_SERVER['REQUEST_METHOD'];
+
+ }
+
+ /**
+ * Returns the requested uri
+ *
+ * @return string
+ */
+ public function getUri() {
+
+ return $this->_SERVER['REQUEST_URI'];
+
+ }
+
+ /**
+ * Will return protocol + the hostname + the uri
+ *
+ * @return string
+ */
+ public function getAbsoluteUri() {
+
+ // Checking if the request was made through HTTPS. The last in line is for IIS
+ $protocol = isset($this->_SERVER['HTTPS']) && ($this->_SERVER['HTTPS']) && ($this->_SERVER['HTTPS']!='off');
+ return ($protocol?'https':'http') . '://' . $this->getHeader('Host') . $this->getUri();
+
+ }
+
+ /**
+ * Returns everything after the ? from the current url
+ *
+ * @return string
+ */
+ public function getQueryString() {
+
+ return isset($this->_SERVER['QUERY_STRING'])?$this->_SERVER['QUERY_STRING']:'';
+
+ }
+
+ /**
+ * Returns the HTTP request body body
+ *
+ * This method returns a readable stream resource.
+ * If the asString parameter is set to true, a string is sent instead.
+ *
+ * @param bool $asString
+ * @return resource
+ */
+ public function getBody($asString = false) {
+
+ if (is_null($this->body)) {
+ if (!is_null(self::$defaultInputStream)) {
+ $this->body = self::$defaultInputStream;
+ } else {
+ $this->body = fopen('php://input','r');
+ self::$defaultInputStream = $this->body;
+ }
+ }
+ if ($asString) {
+ $body = stream_get_contents($this->body);
+ return $body;
+ } else {
+ return $this->body;
+ }
+
+ }
+
+ /**
+ * Sets the contents of the HTTP request body
+ *
+ * This method can either accept a string, or a readable stream resource.
+ *
+ * If the setAsDefaultInputStream is set to true, it means for this run of the
+ * script the supplied body will be used instead of php://input.
+ *
+ * @param mixed $body
+ * @param bool $setAsDefaultInputStream
+ * @return void
+ */
+ public function setBody($body,$setAsDefaultInputStream = false) {
+
+ if(is_resource($body)) {
+ $this->body = $body;
+ } else {
+
+ $stream = fopen('php://temp','r+');
+ fputs($stream,$body);
+ rewind($stream);
+ // String is assumed
+ $this->body = $stream;
+ }
+ if ($setAsDefaultInputStream) {
+ self::$defaultInputStream = $this->body;
+ }
+
+ }
+
+ /**
+ * Returns PHP's _POST variable.
+ *
+ * The reason this is in a method is so it can be subclassed and
+ * overridden.
+ *
+ * @return array
+ */
+ public function getPostVars() {
+
+ return $this->_POST;
+
+ }
+
+ /**
+ * Returns a specific item from the _SERVER array.
+ *
+ * Do not rely on this feature, it is for internal use only.
+ *
+ * @param string $field
+ * @return string
+ */
+ public function getRawServerValue($field) {
+
+ return isset($this->_SERVER[$field])?$this->_SERVER[$field]:null;
+
+ }
+
+ /**
+ * Returns the HTTP version specified within the request.
+ *
+ * @return string
+ */
+ public function getHTTPVersion() {
+
+ $protocol = $this->getRawServerValue('SERVER_PROTOCOL');
+ if ($protocol==='HTTP/1.0') {
+ return '1.0';
+ } else {
+ return '1.1';
+ }
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php
new file mode 100644
index 0000000..07009d9
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Response.php
@@ -0,0 +1,175 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * This class represents a HTTP response.
+ *
+ * It contains the HTTP status code, response headers and a message body.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Response {
+
+ /**
+ * The HTTP version to return in the header() line.
+ *
+ * By default you will definitely want this to be HTTP/1.1, but in some
+ * edge cases (most notably pre 1.2 nginx servers acting as a proxy), you
+ * want to force this to 1.0.
+ *
+ * @var string
+ */
+ public $defaultHttpVersion = '1.1';
+
+ /**
+ * Returns a full HTTP status message for an HTTP status code
+ *
+ * @param int $code
+ * @return string
+ */
+ public function getStatusMessage($code, $httpVersion = '1.1') {
+
+ $msg = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authorative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status', // RFC 4918
+ 208 => 'Already Reported', // RFC 5842
+ 226 => 'IM Used', // RFC 3229
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Reserved',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 418 => 'I\'m a teapot', // RFC 2324
+ 422 => 'Unprocessable Entity', // RFC 4918
+ 423 => 'Locked', // RFC 4918
+ 424 => 'Failed Dependency', // RFC 4918
+ 426 => 'Upgrade required',
+ 428 => 'Precondition required', // draft-nottingham-http-new-status
+ 429 => 'Too Many Requests', // draft-nottingham-http-new-status
+ 431 => 'Request Header Fields Too Large', // draft-nottingham-http-new-status
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version not supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage', // RFC 4918
+ 508 => 'Loop Detected', // RFC 5842
+ 509 => 'Bandwidth Limit Exceeded', // non-standard
+ 510 => 'Not extended',
+ 511 => 'Network Authentication Required', // draft-nottingham-http-new-status
+ );
+
+ return 'HTTP/' . $httpVersion . ' ' . $code . ' ' . $msg[$code];
+
+ }
+
+ // @codeCoverageIgnoreStart
+ // We cannot reasonably test header() related methods.
+
+ /**
+ * Sends an HTTP status header to the client.
+ *
+ * @param int $code HTTP status code
+ * @return bool
+ */
+ public function sendStatus($code) {
+
+ if (!headers_sent())
+ return header($this->getStatusMessage($code, $this->defaultHttpVersion));
+ else return false;
+
+ }
+
+ /**
+ * Sets an HTTP header for the response
+ *
+ * @param string $name
+ * @param string $value
+ * @param bool $replace
+ * @return bool
+ */
+ public function setHeader($name, $value, $replace = true) {
+
+ $value = str_replace(array("\r","\n"),array('\r','\n'),$value);
+ if (!headers_sent())
+ return header($name . ': ' . $value, $replace);
+ else return false;
+
+
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Sets a bunch of HTTP Headers
+ *
+ * headersnames are specified as keys, value in the array value
+ *
+ * @param array $headers
+ * @return void
+ */
+ public function setHeaders(array $headers) {
+
+ foreach($headers as $key=>$value)
+ $this->setHeader($key, $value);
+
+ }
+
+ /**
+ * Sends the entire response body
+ *
+ * This method can accept either an open filestream, or a string.
+ *
+ * @param mixed $body
+ * @return void
+ */
+ public function sendBody($body) {
+
+ if (is_resource($body)) {
+
+ file_put_contents('php://output', $body);
+
+ } else {
+
+ // We assume a string
+ echo $body;
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php
new file mode 100644
index 0000000..d825ad8
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Util.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * HTTP utility methods
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @author Paul Voegler
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Util {
+
+ /**
+ * Parses a RFC2616-compatible date string
+ *
+ * This method returns false if the date is invalid
+ *
+ * @param string $dateHeader
+ * @return bool|DateTime
+ */
+ static function parseHTTPDate($dateHeader) {
+
+ //RFC 2616 section 3.3.1 Full Date
+ //Only the format is checked, valid ranges are checked by strtotime below
+ $month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)';
+ $weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)';
+ $wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)';
+ $time = '[0-2]\d(\:[0-5]\d){2}';
+ $date3 = $month . ' ([1-3]\d| \d)';
+ $date2 = '[0-3]\d\-' . $month . '\-\d\d';
+ //4-digit year cannot begin with 0 - unix timestamp begins in 1970
+ $date1 = '[0-3]\d ' . $month . ' [1-9]\d{3}';
+
+ //ANSI C's asctime() format
+ //4-digit year cannot begin with 0 - unix timestamp begins in 1970
+ $asctime_date = $wkday . ' ' . $date3 . ' ' . $time . ' [1-9]\d{3}';
+ //RFC 850, obsoleted by RFC 1036
+ $rfc850_date = $weekday . ', ' . $date2 . ' ' . $time . ' GMT';
+ //RFC 822, updated by RFC 1123
+ $rfc1123_date = $wkday . ', ' . $date1 . ' ' . $time . ' GMT';
+ //allowed date formats by RFC 2616
+ $HTTP_date = "($rfc1123_date|$rfc850_date|$asctime_date)";
+
+ //allow for space around the string and strip it
+ $dateHeader = trim($dateHeader, ' ');
+ if (!preg_match('/^' . $HTTP_date . '$/', $dateHeader))
+ return false;
+
+ //append implicit GMT timezone to ANSI C time format
+ if (strpos($dateHeader, ' GMT') === false)
+ $dateHeader .= ' GMT';
+
+
+ $realDate = strtotime($dateHeader);
+ //strtotime can return -1 or false in case of error
+ if ($realDate !== false && $realDate >= 0)
+ return new \DateTime('@' . $realDate, new \DateTimeZone('UTC'));
+
+ }
+
+ /**
+ * Transforms a DateTime object to HTTP's most common date format.
+ *
+ * We're serializing it as the RFC 1123 date, which, for HTTP must be
+ * specified as GMT.
+ *
+ * @param \DateTime $dateTime
+ * @return string
+ */
+ static function toHTTPDate(\DateTime $dateTime) {
+
+ // We need to clone it, as we don't want to affect the existing
+ // DateTime.
+ $dateTime = clone $dateTime;
+ $dateTime->setTimeZone(new \DateTimeZone('GMT'));
+ return $dateTime->format('D, d M Y H:i:s \G\M\T');
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php
new file mode 100644
index 0000000..83d5c59
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/HTTP/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\HTTP;
+
+/**
+ * This class contains the OldSabre\HTTP version constants.
+ *
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '1.8.12';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/lib/OldSabre/autoload.php b/calendar/lib/SabreDAV/lib/OldSabre/autoload.php
new file mode 100644
index 0000000..6882472
--- /dev/null
+++ b/calendar/lib/SabreDAV/lib/OldSabre/autoload.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * SabreDAV's autoloader
+ *
+ * This file is kept for backwards compatibility purposes.
+ * SabreDAV now uses the composer autoloader.
+ *
+ * You should stop including this file, and include 'vendor/autoload.php'
+ * instead.
+ *
+ * @deprecated Will be removed in a future version!
+ * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+
+/**
+ * We are assuming that the composer autoloader is just 2 directories up.
+ *
+ * This is not the case when sabredav is installed as a dependency. But, in
+ * those cases it's not expected that people will look for this file anyway.
+ */
+
+require __DIR__ . '/../../vendor/autoload.php';
diff --git a/calendar/lib/SabreDAV/vendor/autoload.php b/calendar/lib/SabreDAV/vendor/autoload.php
new file mode 100644
index 0000000..01c4e4d
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/autoload.php
@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer' . '/autoload_real.php';
+
+return ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5::getLoader();
diff --git a/calendar/lib/SabreDAV/vendor/composer/ClassLoader.php b/calendar/lib/SabreDAV/vendor/composer/ClassLoader.php
new file mode 100644
index 0000000..70d78bc
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/ClassLoader.php
@@ -0,0 +1,387 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ * Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0 class loader
+ *
+ * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md
+ *
+ * $loader = new \Composer\Autoload\ClassLoader();
+ *
+ * // register classes with namespaces
+ * $loader->add('Symfony\Component', __DIR__.'/component');
+ * $loader->add('Symfony', __DIR__.'/framework');
+ *
+ * // activate the autoloader
+ * $loader->register();
+ *
+ * // to enable searching the include path (eg. for PEAR packages)
+ * $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ */
+class ClassLoader
+{
+ // PSR-4
+ private $prefixLengthsPsr4 = array();
+ private $prefixDirsPsr4 = array();
+ private $fallbackDirsPsr4 = array();
+
+ // PSR-0
+ private $prefixesPsr0 = array();
+ private $fallbackDirsPsr0 = array();
+
+ private $useIncludePath = false;
+ private $classMap = array();
+
+ public function getPrefixes()
+ {
+ if (!empty($this->prefixesPsr0)) {
+ return call_user_func_array('array_merge', $this->prefixesPsr0);
+ }
+
+ return array();
+ }
+
+ public function getPrefixesPsr4()
+ {
+ return $this->prefixDirsPsr4;
+ }
+
+ public function getFallbackDirs()
+ {
+ return $this->fallbackDirsPsr0;
+ }
+
+ public function getFallbackDirsPsr4()
+ {
+ return $this->fallbackDirsPsr4;
+ }
+
+ public function getClassMap()
+ {
+ return $this->classMap;
+ }
+
+ /**
+ * @param array $classMap Class to filename map
+ */
+ public function addClassMap(array $classMap)
+ {
+ if ($this->classMap) {
+ $this->classMap = array_merge($this->classMap, $classMap);
+ } else {
+ $this->classMap = $classMap;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix, either
+ * appending or prepending to the ones previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 root directories
+ * @param bool $prepend Whether to prepend the directories
+ */
+ public function add($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ if ($prepend) {
+ $this->fallbackDirsPsr0 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr0
+ );
+ } else {
+ $this->fallbackDirsPsr0 = array_merge(
+ $this->fallbackDirsPsr0,
+ (array) $paths
+ );
+ }
+
+ return;
+ }
+
+ $first = $prefix[0];
+ if (!isset($this->prefixesPsr0[$first][$prefix])) {
+ $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+ return;
+ }
+ if ($prepend) {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixesPsr0[$first][$prefix]
+ );
+ } else {
+ $this->prefixesPsr0[$first][$prefix] = array_merge(
+ $this->prefixesPsr0[$first][$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace, either
+ * appending or prepending to the ones previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-0 base directories
+ * @param bool $prepend Whether to prepend the directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function addPsr4($prefix, $paths, $prepend = false)
+ {
+ if (!$prefix) {
+ // Register directories for the root namespace.
+ if ($prepend) {
+ $this->fallbackDirsPsr4 = array_merge(
+ (array) $paths,
+ $this->fallbackDirsPsr4
+ );
+ } else {
+ $this->fallbackDirsPsr4 = array_merge(
+ $this->fallbackDirsPsr4,
+ (array) $paths
+ );
+ }
+ } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+ // Register directories for a new namespace.
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ } elseif ($prepend) {
+ // Prepend directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ (array) $paths,
+ $this->prefixDirsPsr4[$prefix]
+ );
+ } else {
+ // Append directories for an already registered namespace.
+ $this->prefixDirsPsr4[$prefix] = array_merge(
+ $this->prefixDirsPsr4[$prefix],
+ (array) $paths
+ );
+ }
+ }
+
+ /**
+ * Registers a set of PSR-0 directories for a given prefix,
+ * replacing any others previously set for this prefix.
+ *
+ * @param string $prefix The prefix
+ * @param array|string $paths The PSR-0 base directories
+ */
+ public function set($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr0 = (array) $paths;
+ } else {
+ $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Registers a set of PSR-4 directories for a given namespace,
+ * replacing any others previously set for this namespace.
+ *
+ * @param string $prefix The prefix/namespace, with trailing '\\'
+ * @param array|string $paths The PSR-4 base directories
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setPsr4($prefix, $paths)
+ {
+ if (!$prefix) {
+ $this->fallbackDirsPsr4 = (array) $paths;
+ } else {
+ $length = strlen($prefix);
+ if ('\\' !== $prefix[$length - 1]) {
+ throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+ }
+ $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+ $this->prefixDirsPsr4[$prefix] = (array) $paths;
+ }
+ }
+
+ /**
+ * Turns on searching the include path for class files.
+ *
+ * @param bool $useIncludePath
+ */
+ public function setUseIncludePath($useIncludePath)
+ {
+ $this->useIncludePath = $useIncludePath;
+ }
+
+ /**
+ * Can be used to check if the autoloader uses the include path to check
+ * for classes.
+ *
+ * @return bool
+ */
+ public function getUseIncludePath()
+ {
+ return $this->useIncludePath;
+ }
+
+ /**
+ * Registers this instance as an autoloader.
+ *
+ * @param bool $prepend Whether to prepend the autoloader or not
+ */
+ public function register($prepend = false)
+ {
+ spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+ }
+
+ /**
+ * Unregisters this instance as an autoloader.
+ */
+ public function unregister()
+ {
+ spl_autoload_unregister(array($this, 'loadClass'));
+ }
+
+ /**
+ * Loads the given class or interface.
+ *
+ * @param string $class The name of the class
+ * @return bool|null True if loaded, null otherwise
+ */
+ public function loadClass($class)
+ {
+ if ($file = $this->findFile($class)) {
+ includeFile($file);
+
+ return true;
+ }
+ }
+
+ /**
+ * Finds the path to the file where the class is defined.
+ *
+ * @param string $class The name of the class
+ *
+ * @return string|false The path if found, false otherwise
+ */
+ public function findFile($class)
+ {
+ // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
+ if ('\\' == $class[0]) {
+ $class = substr($class, 1);
+ }
+
+ // class map lookup
+ if (isset($this->classMap[$class])) {
+ return $this->classMap[$class];
+ }
+
+ $file = $this->findFileWithExtension($class, '.php');
+
+ // Search for Hack files if we are running on HHVM
+ if ($file === null && defined('HHVM_VERSION')) {
+ $file = $this->findFileWithExtension($class, '.hh');
+ }
+
+ if ($file === null) {
+ // Remember that this class does not exist.
+ return $this->classMap[$class] = false;
+ }
+
+ return $file;
+ }
+
+ private function findFileWithExtension($class, $ext)
+ {
+ // PSR-4 lookup
+ $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+ $first = $class[0];
+ if (isset($this->prefixLengthsPsr4[$first])) {
+ foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-4 fallback dirs
+ foreach ($this->fallbackDirsPsr4 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 lookup
+ if (false !== $pos = strrpos($class, '\\')) {
+ // namespaced class name
+ $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+ . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+ } else {
+ // PEAR-like class name
+ $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+ }
+
+ if (isset($this->prefixesPsr0[$first])) {
+ foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+ if (0 === strpos($class, $prefix)) {
+ foreach ($dirs as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+ }
+ }
+ }
+
+ // PSR-0 fallback dirs
+ foreach ($this->fallbackDirsPsr0 as $dir) {
+ if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+ return $file;
+ }
+ }
+
+ // PSR-0 include paths.
+ if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+ return $file;
+ }
+ }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+ include $file;
+}
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php b/calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php
new file mode 100644
index 0000000..7a91153
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_classmap.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php b/calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php
new file mode 100644
index 0000000..f442fdb
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_namespaces.php
@@ -0,0 +1,15 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+ 'OldSabre\\VObject' => array($vendorDir . '/oldsabre/vobject/lib'),
+ 'OldSabre\\HTTP' => array($baseDir . '/lib'),
+ 'OldSabre\\DAVACL' => array($baseDir . '/lib'),
+ 'OldSabre\\DAV' => array($baseDir . '/lib'),
+ 'OldSabre\\CardDAV' => array($baseDir . '/lib'),
+ 'OldSabre\\CalDAV' => array($baseDir . '/lib'),
+);
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php b/calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php
new file mode 100644
index 0000000..b265c64
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_psr4.php
@@ -0,0 +1,9 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);
diff --git a/calendar/lib/SabreDAV/vendor/composer/autoload_real.php b/calendar/lib/SabreDAV/vendor/composer/autoload_real.php
new file mode 100644
index 0000000..492c0bf
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/autoload_real.php
@@ -0,0 +1,50 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5
+{
+ private static $loader;
+
+ public static function loadClassLoader($class)
+ {
+ if ('Composer\Autoload\ClassLoader' === $class) {
+ require __DIR__ . '/ClassLoader.php';
+ }
+ }
+
+ public static function getLoader()
+ {
+ if (null !== self::$loader) {
+ return self::$loader;
+ }
+
+ spl_autoload_register(array('ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5', 'loadClassLoader'), true, true);
+ self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+ spl_autoload_unregister(array('ComposerAutoloaderInit339e11a867637abea2ce832a4777d9e5', 'loadClassLoader'));
+
+ $map = require __DIR__ . '/autoload_namespaces.php';
+ foreach ($map as $namespace => $path) {
+ $loader->set($namespace, $path);
+ }
+
+ $map = require __DIR__ . '/autoload_psr4.php';
+ foreach ($map as $namespace => $path) {
+ $loader->setPsr4($namespace, $path);
+ }
+
+ $classMap = require __DIR__ . '/autoload_classmap.php';
+ if ($classMap) {
+ $loader->addClassMap($classMap);
+ }
+
+ $loader->register(true);
+
+ return $loader;
+ }
+}
+
+function composerRequire339e11a867637abea2ce832a4777d9e5($file)
+{
+ require $file;
+}
diff --git a/calendar/lib/SabreDAV/vendor/composer/installed.json b/calendar/lib/SabreDAV/vendor/composer/installed.json
new file mode 100644
index 0000000..b4f0389
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/composer/installed.json
@@ -0,0 +1,52 @@
+[
+ {
+ "name": "sabre/vobject",
+ "version": "2.1.7",
+ "version_normalized": "2.1.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruux/sabre-vobject.git",
+ "reference": "c3ebe643fd1037b656ebcc149e3fd3d38938fe58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/c3ebe643fd1037b656ebcc149e3fd3d38938fe58",
+ "reference": "c3ebe643fd1037b656ebcc149e3fd3d38938fe58",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=5.3.1"
+ },
+ "time": "2015-01-21 20:57:59",
+ "bin": [
+ "bin/vobjectvalidate.php"
+ ],
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "OldSabre\\VObject": "lib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Evert Pot",
+ "email": "evert@rooftopsolutions.nl",
+ "homepage": "http://evertpot.com/",
+ "role": "Developer"
+ }
+ ],
+ "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "homepage": "http://sabre.io/vobject/",
+ "keywords": [
+ "VObject",
+ "iCalendar",
+ "vCard"
+ ]
+ }
+]
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog
new file mode 100644
index 0000000..c330fa2
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/ChangeLog
@@ -0,0 +1,70 @@
+2.1.0-stable (2013-06-17)
+ * This version is fully backwards compatible with 2.0.*. However, it
+ contains a few new API's that mimic the VObject 3 API. This allows it to
+ be used a 'bridge' version.
+ Specifically, this new version exists so SabreDAV 1.7 and 1.8 can run with
+ both the 2 and 3 versions of this library.
+ * Added: Property\DateTime::hasTime().
+ * Added: Property\MultiDateTime::hasTime().
+ * Added: Property::getValue().
+ * Added: Document class.
+ * Added: Document::createComponent and Document::createProperty.
+ * Added: Parameter::getValue().
+
+
+2.0.7-stable (2013-03-05)
+ * Fixed: Microsoft re-uses their magic numbers for different timezones,
+ specifically id 2 for both Sarajevo and Lisbon). A workaround was added
+ to deal with this.
+
+2.0.6-stable (2013-02-17)
+ * Fixed: The reader now properly parses parameters without a value.
+
+2.0.5-stable (2012-11-05)
+ * Fixed: The FreeBusyGenerator is now properly using the factory methods
+ for creation of components and properties.
+
+2.0.4-stable (2012-11-02)
+ * Added: Known Lotus Notes / Domino timezone id's.
+
+2.0.3-stable (2012-10-29)
+ * Added: Support for 'GMT+????' format in TZID's.
+ * Added: Support for formats like SystemV/EST5EDT in TZID's.
+ * Fixed: RecurrenceIterator now repairs recurrence rules where UNTIL < DTSTART.
+ * Added: Support for BYHOUR in FREQ=DAILY (@hollodk).
+ * Added: Support for BYHOUR and BYDAY in FREQ=WEEKLY.
+
+2.0.2-stable (2012-10-06)
+ * Added: includes.php file, to load the entire library in one go.
+ * Fixed: A problem with determining alarm triggers for TODO's.
+
+2.0.1-stable (2012-09-22)
+ * Removed: Element class. It wasn't used.
+ * Added: Basic validation and repair methods for broken input data.
+ * Fixed: RecurrenceIterator could infinitely loop when an INTERVAL of 0
+ was specified.
+ * Added: A cli script that can validate and automatically repair vcards
+ and iCalendar objects.
+ * Added: A new 'Compound' property, that can automatically split up parts
+ for properties such as N, ADR, ORG and CATEGORIES.
+ * Added: Splitter classes, that can split up large objects (such as exports)
+ into individual objects (thanks @DominikTO and @armin-hackmann).
+ * Added: VFREEBUSY component, which allows easily checking wether
+ timeslots are available.
+ * Added: The Reader class now has a 'FORGIVING' option, which allows it to
+ parse properties with incorrect characters in the name (at this time, it
+ just allows underscores).
+ * Added: Also added the 'IGNORE_INVALID_LINES' option, to completely
+ disregard any invalid lines.
+ * Fixed: A bug in Windows timezone-id mappings for times created in
+ Greenlands timezone (sorry Greenlanders! I do care!).
+ * Fixed: DTEND was not generated correctly for VFREEBUSY reports.
+ * Fixed: Parser is at least 25% faster with real-world data.
+
+2.0.0-stable (2012-08-08)
+ * VObject is now a separate project from SabreDAV. See the SabreDAV
+ changelog for version information before 2.0.
+ * New: VObject library now uses PHP 5.3 namespaces.
+ * New: It's possible to specify lists of parameters when constructing
+ properties.
+ * New: made it easier to construct the FreeBusyGenerator.
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE
new file mode 100644
index 0000000..628c60c
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/LICENSE
@@ -0,0 +1,27 @@
+Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/)
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name Sabre nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md
new file mode 100644
index 0000000..e845cec
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/README.md
@@ -0,0 +1,384 @@
+SabreTooth VObject library
+==========================
+
+[![Build Status](https://secure.travis-ci.org/fruux/sabre-vobject.png?branch=master)](http://travis-ci.org/fruux/sabre-vobject)
+
+The VObject library allows you to easily parse and manipulate [iCalendar](https://tools.ietf.org/html/rfc5545)
+and [vCard](https://tools.ietf.org/html/rfc6350) objects using PHP.
+The goal of the VObject library is to create a very complete library, with an easy to use API.
+
+This project is a spin-off from [SabreDAV](http://code.google.com/p/sabredav/), where it has
+been used for several years. The VObject library has 100% unittest coverage.
+
+Installation
+------------
+
+VObject requires PHP 5.3, and should be installed using composer.
+The general composer instructions can be found on the [composer website](http://getcomposer.org/doc/00-intro.md composer website).
+
+After that, just declare the vobject dependency as follows:
+
+```
+"require" : {
+ "sabre/vobject" : "2.0.*"
+}
+```
+
+Then, run `composer.phar update` and you should be good.
+
+Usage
+-----
+
+### Parsing
+
+For our example, we will be using the following vcard:
+
+```
+BEGIN:VCARD
+VERSION:3.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+N:Planck;Max;;;
+FN:Max Planck
+EMAIL;TYPE=WORK:mplanck@example.org
+item1.TEL;TYPE=CELL:(+49)3144435678
+item1.X-ABLabel:Private cell
+item2.TEL;TYPE=WORK:(+49)5554564744
+item2.X-ABLabel:Work
+END:VCARD
+```
+
+
+If we want to just print out Max' full name, you can just use property access:
+
+
+```php
+use OldSabre\VObject;
+
+$card = VObject\Reader::read($data);
+echo $card->FN;
+```
+
+### Changing properties
+
+Creating properties is pretty similar. If we like to add his birthday, we just
+set the property:
+
+```php
+$card->BDAY = '1858-04-23';
+```
+
+Note that in the previous example, we're actually updating any existing BDAY that
+may already exist. If we want to add a new property, without overwriting the previous
+we can do this with the `add` method.
+
+```php
+$card->add('EMAIL','max@example.org');
+```
+
+### Parameters
+
+If we want to also specify that this is max' home email addresses, we can do this with
+a third parameter:
+
+```
+$card->add('EMAIL', 'max@example'org', array('type' => 'HOME'));
+```
+
+If we want to read out all the email addresses and their type, this would look something
+like this:
+
+```
+foreach($card->EMAIL as $email) {
+
+ echo $email['TYPE'], ' - ', $email;
+
+}
+```
+
+### Groups
+
+In our example, you can see that the TEL properties are prefixed. These are 'groups' and
+allow you to group multiple related properties together. The group can be any user-defined
+name.
+
+This particular example as generated by the OS X addressbook, which uses the `X-ABLabel`
+to allow the user to specify custom labels for properties. OS X addressbook uses groups
+to tie the label to the property.
+
+The VObject library simply ignores the group if you don't specify it, so this will work:
+
+```php
+foreach($card->TEL as $tel) {
+ echo $tel, "\n";
+}
+```
+
+But if you would like to target a specific group + property, this is possible too:
+
+```php
+echo $card->{'ITEM1.TEL'};
+```
+
+So if you would like to show all the phone numbers, along with their custom label, the
+following syntax is used:
+
+```php
+foreach($card->TEL as $tel) {
+
+ echo $card->{$tel->group . '.X-ABLABEL'}, ": ";
+ echo $tel, "\n";
+
+}
+```
+
+### Serializing / Saving
+
+If you want to generate your updated VObject, you can simply call the serialize() method.
+
+```php
+echo $card->serialize();
+```
+
+### Components
+
+iCalendar, unlike vCards always have sub-components. Where vCards are often just a flat
+list, iCalendar objects tend to have a tree-like structure. For the following paragraphs,
+we will use the following object as the example:
+
+```
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+BEGIN:VEVENT
+SUMMARY:Curiosity landing
+DTSTART:20120806T051439Z
+LOCATION:Mars
+END:VEVENT
+END:VCALENDAR
+```
+
+Since events, tasks and journals are always in a sub component, this is also how we
+access them.
+
+```php
+use OldSabre\VObject;
+
+$calendar = VObject\Reader::read($data);
+echo $calendar->VEVENT->SUMMARY;
+```
+
+Adding components to a calendar is done with a factory method:
+
+```php
+$event = VObject\Component::create('VEVENT');
+$calendar->add($event);
+
+$event->SUMMARY = 'Curiosity launch';
+$event->DTSTART = '20111126T150202Z';
+$event->LOCATION = 'Cape Carnival';
+```
+
+By the way.. cloning also works as expected, as the entire structure is cloned along with it:
+
+```php
+$clonedEvent = clone $calendar->VEVENT[0];
+$calendar->add($clonedEvent);
+```
+
+### Date and time handling
+
+If you ever had to deal with iCalendar timezones, you know it can be complicated.
+The way timezones are specified is flawed, which is something I may write an essay about some
+day. VObject does its best to determine the correct timezone. Many standard formats
+have been tested and verified, and special code has been implemented for special-casing
+microsoft generated timezone information, and others.
+
+To get a real php `DateTime` object, you can request this as follows:
+
+```php
+$event = $calendar->VEVENT;
+$start = $event->DTSTART->getDateTime();
+echo $start->format(\DateTime::W3C);
+```
+
+To set the property with a DateTime object, you can use the following syntax:
+
+```php
+$dateTime = new \DateTime('2012-08-07 23:53:00', new DateTimeZone('Europe/Amsterdam'));
+$event->DTSTART->setDateTime($dateTime, VObject\Property\DateTime::DATE);
+```
+
+The second argument specifies the type of date you're setting. The following three
+options exist:
+
+1. `LOCAL` This is a floating time, with no timezone information. This basically specifies that the event happens in whatever the timezone's currently in. This would be encoded as `DTSTART;VALUE=DATE-TIME:20120807235300`
+2. `UTC` This specifies that the time should be encoded as a UTC time. This is encoded as `DTSTART;VALUE=DATE-TIME:20120807205300Z`. Note the extra Z and the fact that it's two hours 'earlier'.
+3. `LOCALTZ` specifies that it's supposed to be encoded in its local timezone. For example `DTSTART;VALUE=DATE-TIME;TZID=Europe/Amsterdam:20120807235300`.
+4. `DATE` This is a date-only, and does not contain the time. In this case this would be encoded as `DTSTART;VALUE=DATE:20120807`.
+
+A few important notes:
+
+* When a `TZID` is specified, there should also be a matching `VTIMEZONE` object with all the timezone information. VObject cannot currently automatically embed this. However, in reality other clients seem to do fine without this information. Yet, for completeness, this will be added in the future.
+* As mentioned, the timezone-determination process may sometimes fail. Report any issues you find, and I'll be quick to add workarounds!
+
+### Recurrence rules
+
+Recurrence rules allow events to recur, for example for a weekly meeting, or an anniversary.
+This is done with the `RRULE` property. The `RRULE` property allows for a LOT of different
+rules. VObject only implements the ones that actually appear in calendar software.
+
+To read more about `RRULE` and all the options, check out [RFC5545](https://tools.ietf.org/html/rfc5545#section-3.8.5).
+VObject supports the following options:
+
+1. `UNTIL` for an end date.
+2. `INTERVAL` for for example "every 2 days".
+3. `COUNT` to stop recurring after x items.
+4. `FREQ=DAILY` to recur every day, and `BYDAY` to limit it to certain days.
+5. `FREQ=WEEKLY` to recur every week, `BYDAY` to expand this to multiple weekdays in every week and `WKST` to specify on which day the week starts.
+6. `FREQ=MONTHLY` to recur every month, `BYMONTHDAY` to expand this to certain days in a month, `BYDAY` to expand it to certain weekdays occuring in a month, and `BYSETPOS` to limit the last two expansions.
+7. `FREQ=YEARLY` to recur every year, `BYMONTH` to expand that to certain months in a year, and `BYDAY` and `BYWEEKDAY` to expand the `BYMONTH` rule even further.
+
+VObject supports the `EXDATE` property for exclusions, but not yet the `RDATE` and `EXRULE`
+properties. If you're interested in this, please file a github issue, as this will put it
+on my radar.
+
+This is a bit of a complex subject to go in excruciating detail. The
+[RFC](https://tools.ietf.org/html/rfc5545#section-3.8.5) has a lot of examples though.
+
+The hard part is not to write the RRULE, it is to expand them. The most complex and
+hard-to-read code is hidden in this component. Dragons be here.
+
+So, if we have a meeting every 2nd monday of the month, this would be specified as such:
+
+```
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+BEGIN:VEVENT
+UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+DTSTART:20120109T140000Z
+RRULE:FREQ=MONTHLY;BYDAY=MO;BYSETPOS=2
+END:VEVENT
+END:VCALENDAR
+```
+
+Note that normally it's not allowed to indent the object like this, but it does make
+it easier to read. This is also the first time I added in a UID, which is required
+for all VEVENT, VTODO and VJOURNAL objects!
+
+To figure out all the meetings for this year, we can use the following syntax:
+
+```php
+use OldSabre\VObject;
+
+$calendar = VObject\Reader::read($data);
+$calendar->expand(new DateTime('2012-01-01'), new DateTime('2012-12-31'));
+```
+
+What the expand method does, is look at its inner events, and expand the recurring
+rule. Our calendar now contains 12 events. The first will have its RRULE stripped,
+and every subsequent VEVENT has the correct meeting date and a `RECURRENCE-ID` set.
+
+This results in something like this:
+
+```
+BEGIN:VCALENDAR
+ VERSION:2.0
+ PRODID:-//Sabre//Sabre VObject 2.0//EN
+ BEGIN:VEVENT
+ UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+ DTSTART:20120109T140000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+ RECURRENCE-ID:20120213T140000Z
+ DTSTART:20120213T140000Z
+ END:VEVENT
+ BEGIN:VEVENT
+ UID:1102c450-e0d7-11e1-9b23-0800200c9a66
+ RECURRENCE-ID:20120312T140000Z
+ DTSTART:20120312T140000Z
+ END:VEVENT
+ ..etc..
+END:VCALENDAR
+```
+
+To show the list of dates, we would do this as such:
+
+```php
+foreach($calendar->VEVENT as $event) {
+ echo $event->DTSTART->getDateTime()->format(\DateTime::ATOM);
+}
+```
+
+In a recurring event, single instances can also be overriden. VObject also takes these
+into consideration. The reason we needed to specify a start and end-date, is because
+some recurrence rules can be 'never ending'.
+
+You should make sure you pick a sane date-range. Because if you pick a 50 year
+time-range, for a daily recurring event; this would result in over 18K objects.
+
+Free-busy report generation
+---------------------------
+
+Some calendaring software can make use of FREEBUSY reports to show when people are
+available.
+
+You can automatically generate these reports from calendars using the `FreeBusyGenerator`.
+
+Example based on our last event:
+
+```php
+// We're giving it the calendar object. It's also possible to specify multiple objects,
+// by setting them as an array.
+//
+// We must also specify a start and end date, because recurring events are expanded.
+$fbGenerator = new VObject\FreeBusyGenerator(
+ new DateTime('2012-01-01'),
+ new DateTime('2012-12-31'),
+ $calendar
+);
+
+// Grabbing the report
+$freebusy = $fbGenerator->result();
+
+// The freebusy report is another VCALENDAR object, so we can serialize it as usual:
+echo $freebusy->serialize();
+```
+
+The output of this script will look like this:
+
+```
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Sabre//Sabre VObject 2.0//EN
+CALSCALE:GREGORIAN
+BEGIN:VFREEBUSY
+DTSTART;VALUE=DATE-TIME:20111231T230000Z
+DTEND;VALUE=DATE-TIME:20111231T230000Z
+DTSTAMP;VALUE=DATE-TIME:20120808T131628Z
+FREEBUSY;FBTYPE=BUSY:20120109T140000Z/20120109T140000Z
+FREEBUSY;FBTYPE=BUSY:20120213T140000Z/20120213T140000Z
+FREEBUSY;FBTYPE=BUSY:20120312T140000Z/20120312T140000Z
+FREEBUSY;FBTYPE=BUSY:20120409T140000Z/20120409T140000Z
+FREEBUSY;FBTYPE=BUSY:20120514T140000Z/20120514T140000Z
+FREEBUSY;FBTYPE=BUSY:20120611T140000Z/20120611T140000Z
+FREEBUSY;FBTYPE=BUSY:20120709T140000Z/20120709T140000Z
+FREEBUSY;FBTYPE=BUSY:20120813T140000Z/20120813T140000Z
+FREEBUSY;FBTYPE=BUSY:20120910T140000Z/20120910T140000Z
+FREEBUSY;FBTYPE=BUSY:20121008T140000Z/20121008T140000Z
+FREEBUSY;FBTYPE=BUSY:20121112T140000Z/20121112T140000Z
+FREEBUSY;FBTYPE=BUSY:20121210T140000Z/20121210T140000Z
+END:VFREEBUSY
+END:VCALENDAR
+```
+
+Support
+-------
+
+Head over to the [SabreDAV mailing list](http://groups.google.com/group/sabredav-discuss) for any questions.
+
+Made at fruux
+-------------
+
+This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json
new file mode 100644
index 0000000..0a83d89
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/composer.json
@@ -0,0 +1,31 @@
+{
+ "name": "sabre/vobject",
+ "description" : "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+ "keywords" : [ "VObject", "iCalendar", "vCard" ],
+ "homepage" : "https://github.com/fruux/sabre-vobject",
+ "license" : "BSD-3-Clause",
+ "require" : {
+ "php" : ">=5.3.1",
+ "ext-mbstring" : "*"
+ },
+ "authors" : [
+ {
+ "name" : "Evert Pot",
+ "email" : "evert@rooftopsolutions.nl",
+ "homepage" : "http://evertpot.com/",
+ "role" : "Developer"
+ }
+ ],
+ "support" : {
+ "forum" : "https://groups.google.com/group/sabredav-discuss",
+ "source" : "https://github.com/fruux/sabre-vobject"
+ },
+ "autoload" : {
+ "psr-0" : {
+ "OldSabre\\VObject" : "lib/"
+ }
+ },
+ "bin" : [
+ "bin/vobjectvalidate.php"
+ ]
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php
new file mode 100644
index 0000000..f4921ac
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component.php
@@ -0,0 +1,405 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Component
+ *
+ * This class represents a VCALENDAR/VCARD component. A component is for example
+ * VEVENT, VTODO and also VCALENDAR. It starts with BEGIN:COMPONENTNAME and
+ * ends with END:COMPONENTNAME
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Component extends Node {
+
+ /**
+ * Name, for example VEVENT
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Children properties and components
+ *
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * If components are added to this map, they will be automatically mapped
+ * to their respective classes, if parsed by the reader or constructed with
+ * the 'create' method.
+ *
+ * @var array
+ */
+ static public $classMap = array(
+ 'VALARM' => 'OldSabre\\VObject\\Component\\VAlarm',
+ 'VCALENDAR' => 'OldSabre\\VObject\\Component\\VCalendar',
+ 'VCARD' => 'OldSabre\\VObject\\Component\\VCard',
+ 'VEVENT' => 'OldSabre\\VObject\\Component\\VEvent',
+ 'VJOURNAL' => 'OldSabre\\VObject\\Component\\VJournal',
+ 'VTODO' => 'OldSabre\\VObject\\Component\\VTodo',
+ 'VFREEBUSY' => 'OldSabre\\VObject\\Component\\VFreeBusy',
+ );
+
+ /**
+ * Creates the new component by name, but in addition will also see if
+ * there's a class mapped to the property name.
+ *
+ * @param string $name
+ * @param string $value
+ * @return Component
+ */
+ static public function create($name, $value = null) {
+
+ $name = strtoupper($name);
+
+ if (isset(self::$classMap[$name])) {
+ return new self::$classMap[$name]($name, $value);
+ } else {
+ return new self($name, $value);
+ }
+
+ }
+
+ /**
+ * Creates a new component.
+ *
+ * By default this object will iterate over its own children, but this can
+ * be overridden with the iterator argument
+ *
+ * @param string $name
+ * @param ElementList $iterator
+ */
+ public function __construct($name, ElementList $iterator = null) {
+
+ $this->name = strtoupper($name);
+ if (!is_null($iterator)) $this->iterator = $iterator;
+
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ $str = "BEGIN:" . $this->name . "\r\n";
+
+ /**
+ * Gives a component a 'score' for sorting purposes.
+ *
+ * This is solely used by the childrenSort method.
+ *
+ * A higher score means the item will be lower in the list.
+ * To avoid score collisions, each "score category" has a reasonable
+ * space to accomodate elements. The $key is added to the $score to
+ * preserve the original relative order of elements.
+ *
+ * @param int $key
+ * @param array $array
+ * @return int
+ */
+ $sortScore = function($key, $array) {
+
+ if ($array[$key] instanceof Component) {
+
+ // We want to encode VTIMEZONE first, this is a personal
+ // preference.
+ if ($array[$key]->name === 'VTIMEZONE') {
+ $score=300000000;
+ return $score+$key;
+ } else {
+ $score=400000000;
+ return $score+$key;
+ }
+ } else {
+ // Properties get encoded first
+ // VCARD version 4.0 wants the VERSION property to appear first
+ if ($array[$key] instanceof Property) {
+ if ($array[$key]->name === 'VERSION') {
+ $score=100000000;
+ return $score+$key;
+ } else {
+ // All other properties
+ $score=200000000;
+ return $score+$key;
+ }
+ }
+ }
+
+ };
+
+ $tmp = $this->children;
+ uksort($this->children, function($a, $b) use ($sortScore, $tmp) {
+
+ $sA = $sortScore($a, $tmp);
+ $sB = $sortScore($b, $tmp);
+
+ if ($sA === $sB) return 0;
+
+ return ($sA < $sB) ? -1 : 1;
+
+ });
+
+ foreach($this->children as $child) $str.=$child->serialize();
+ $str.= "END:" . $this->name . "\r\n";
+
+ return $str;
+
+ }
+
+ /**
+ * Adds a new component or element
+ *
+ * You can call this method with the following syntaxes:
+ *
+ * add(Node $node)
+ * add(string $name, $value, array $parameters = array())
+ *
+ * The first version adds an Element
+ * The second adds a property as a string.
+ *
+ * @param mixed $item
+ * @param mixed $itemValue
+ * @return void
+ */
+ public function add($item, $itemValue = null, array $parameters = array()) {
+
+ if ($item instanceof Node) {
+ if (!is_null($itemValue)) {
+ throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
+ }
+ $item->parent = $this;
+ $this->children[] = $item;
+ } elseif(is_string($item)) {
+
+ $item = Property::create($item,$itemValue, $parameters);
+ $item->parent = $this;
+ $this->children[] = $item;
+
+ } else {
+
+ throw new \InvalidArgumentException('The first argument must either be a \\OldSabre\\VObject\\Node or a string');
+
+ }
+
+ }
+
+ /**
+ * Returns an iterable list of children
+ *
+ * @return ElementList
+ */
+ public function children() {
+
+ return new ElementList($this->children);
+
+ }
+
+ /**
+ * Returns an array with elements that match the specified name.
+ *
+ * This function is also aware of MIME-Directory groups (as they appear in
+ * vcards). This means that if a property is grouped as "HOME.EMAIL", it
+ * will also be returned when searching for just "EMAIL". If you want to
+ * search for a property in a specific group, you can select on the entire
+ * string ("HOME.EMAIL"). If you want to search on a specific property that
+ * has not been assigned a group, specify ".EMAIL".
+ *
+ * Keys are retained from the 'children' array, which may be confusing in
+ * certain cases.
+ *
+ * @param string $name
+ * @return array
+ */
+ public function select($name) {
+
+ $group = null;
+ $name = strtoupper($name);
+ if (strpos($name,'.')!==false) {
+ list($group,$name) = explode('.', $name, 2);
+ }
+
+ $result = array();
+ foreach($this->children as $key=>$child) {
+
+ if (
+ strtoupper($child->name) === $name &&
+ (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group))
+ ) {
+
+ $result[$key] = $child;
+
+ }
+ }
+
+ reset($result);
+ return $result;
+
+ }
+
+ /**
+ * This method only returns a list of sub-components. Properties are
+ * ignored.
+ *
+ * @return array
+ */
+ public function getComponents() {
+
+ $result = array();
+ foreach($this->children as $child) {
+ if ($child instanceof Component) {
+ $result[] = $child;
+ }
+ }
+
+ return $result;
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $result = array();
+ foreach($this->children as $child) {
+ $result = array_merge($result, $child->validate($options));
+ }
+ return $result;
+
+ }
+
+ /* Magic property accessors {{{ */
+
+ /**
+ * Using 'get' you will either get a property or component,
+ *
+ * If there were no child-elements found with the specified name,
+ * null is returned.
+ *
+ * @param string $name
+ * @return Property
+ */
+ public function __get($name) {
+
+ $matches = $this->select($name);
+ if (count($matches)===0) {
+ return null;
+ } else {
+ $firstMatch = current($matches);
+ /** @var $firstMatch Property */
+ $firstMatch->setIterator(new ElementList(array_values($matches)));
+ return $firstMatch;
+ }
+
+ }
+
+ /**
+ * This method checks if a sub-element with the specified name exists.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function __isset($name) {
+
+ $matches = $this->select($name);
+ return count($matches)>0;
+
+ }
+
+ /**
+ * Using the setter method you can add properties or subcomponents
+ *
+ * You can either pass a Component, Property
+ * object, or a string to automatically create a Property.
+ *
+ * If the item already exists, it will be removed. If you want to add
+ * a new item with the same name, always use the add() method.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function __set($name, $value) {
+
+ $matches = $this->select($name);
+ $overWrite = count($matches)?key($matches):null;
+
+ if ($value instanceof Component || $value instanceof Property) {
+ $value->parent = $this;
+ if (!is_null($overWrite)) {
+ $this->children[$overWrite] = $value;
+ } else {
+ $this->children[] = $value;
+ }
+ } elseif (is_scalar($value)) {
+ $property = Property::create($name,$value);
+ $property->parent = $this;
+ if (!is_null($overWrite)) {
+ $this->children[$overWrite] = $property;
+ } else {
+ $this->children[] = $property;
+ }
+ } else {
+ throw new \InvalidArgumentException('You must pass a \\OldSabre\\VObject\\Component, \\OldSabre\\VObject\\Property or scalar type');
+ }
+
+ }
+
+ /**
+ * Removes all properties and components within this component.
+ *
+ * @param string $name
+ * @return void
+ */
+ public function __unset($name) {
+
+ $matches = $this->select($name);
+ foreach($matches as $k=>$child) {
+
+ unset($this->children[$k]);
+ $child->parent = null;
+
+ }
+
+ }
+
+ /* }}} */
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ *
+ * @return void
+ */
+ public function __clone() {
+
+ foreach($this->children as $key=>$child) {
+ $this->children[$key] = clone $child;
+ $this->children[$key]->parent = $this;
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php
new file mode 100644
index 0000000..e5aed86
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VAlarm.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+use OldSabre\VObject;
+
+/**
+ * VAlarm component
+ *
+ * This component contains some additional functionality specific for VALARMs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VAlarm extends VObject\Component {
+
+ /**
+ * Returns a DateTime object when this alarm is going to trigger.
+ *
+ * This ignores repeated alarm, only the first trigger is returned.
+ *
+ * @return DateTime
+ */
+ public function getEffectiveTriggerTime() {
+
+ $trigger = $this->TRIGGER;
+ if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') {
+ $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
+ $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START';
+
+ $parentComponent = $this->parent;
+ if ($related === 'START') {
+
+ if ($parentComponent->name === 'VTODO') {
+ $propName = 'DUE';
+ } else {
+ $propName = 'DTSTART';
+ }
+
+ $effectiveTrigger = clone $parentComponent->$propName->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ } else {
+ if ($parentComponent->name === 'VTODO') {
+ $endProp = 'DUE';
+ } elseif ($parentComponent->name === 'VEVENT') {
+ $endProp = 'DTEND';
+ } else {
+ throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
+ }
+
+ if (isset($parentComponent->$endProp)) {
+ $effectiveTrigger = clone $parentComponent->$endProp->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ } elseif (isset($parentComponent->DURATION)) {
+ $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+ $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
+ $effectiveTrigger->add($duration);
+ $effectiveTrigger->add($triggerDuration);
+ } else {
+ $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+ $effectiveTrigger->add($triggerDuration);
+ }
+ }
+ } else {
+ $effectiveTrigger = $trigger->getDateTime();
+ }
+ return $effectiveTrigger;
+
+ }
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $effectiveTrigger = $this->getEffectiveTriggerTime();
+
+ if (isset($this->DURATION)) {
+ $duration = VObject\DateTimeParser::parseDuration($this->DURATION);
+ $repeat = (string)$this->repeat;
+ if (!$repeat) {
+ $repeat = 1;
+ }
+
+ $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat);
+
+ foreach($period as $occurrence) {
+
+ if ($start <= $occurrence && $end > $occurrence) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return ($start <= $effectiveTrigger && $end > $effectiveTrigger);
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php
new file mode 100644
index 0000000..e80871c
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCalendar.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VCalendar component
+ *
+ * This component adds functionality to a component, specific for a VCALENDAR.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCalendar extends VObject\Document {
+
+ static $defaultName = 'VCALENDAR';
+
+ /**
+ * Returns a list of all 'base components'. For instance, if an Event has
+ * a recurrence rule, and one instance is overridden, the overridden event
+ * will have the same UID, but will be excluded from this list.
+ *
+ * VTIMEZONE components will always be excluded.
+ *
+ * @param string $componentName filter by component name
+ * @return array
+ */
+ public function getBaseComponents($componentName = null) {
+
+ $components = array();
+ foreach($this->children as $component) {
+
+ if (!$component instanceof VObject\Component)
+ continue;
+
+ if (isset($component->{'RECURRENCE-ID'}))
+ continue;
+
+ if ($componentName && $component->name !== strtoupper($componentName))
+ continue;
+
+ if ($component->name === 'VTIMEZONE')
+ continue;
+
+ $components[] = $component;
+
+ }
+
+ return $components;
+
+ }
+
+ /**
+ * If this calendar object, has events with recurrence rules, this method
+ * can be used to expand the event into multiple sub-events.
+ *
+ * Each event will be stripped from it's recurrence information, and only
+ * the instances of the event in the specified timerange will be left
+ * alone.
+ *
+ * In addition, this method will cause timezone information to be stripped,
+ * and normalized to UTC.
+ *
+ * This method will alter the VCalendar. This cannot be reversed.
+ *
+ * This functionality is specifically used by the CalDAV standard. It is
+ * possible for clients to request expand events, if they are rather simple
+ * clients and do not have the possibility to calculate recurrences.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return void
+ */
+ public function expand(\DateTime $start, \DateTime $end) {
+
+ $newEvents = array();
+
+ foreach($this->select('VEVENT') as $key=>$vevent) {
+
+ if (isset($vevent->{'RECURRENCE-ID'})) {
+ unset($this->children[$key]);
+ continue;
+ }
+
+
+ if (!$vevent->rrule) {
+ unset($this->children[$key]);
+ if ($vevent->isInTimeRange($start, $end)) {
+ $newEvents[] = $vevent;
+ }
+ continue;
+ }
+
+ $uid = (string)$vevent->uid;
+ if (!$uid) {
+ throw new \LogicException('Event did not have a UID!');
+ }
+
+ $it = new VObject\RecurrenceIterator($this, $vevent->uid);
+ $it->fastForward($start);
+
+ while($it->valid() && $it->getDTStart() < $end) {
+
+ if ($it->getDTEnd() > $start) {
+
+ $newEvents[] = $it->getEventObject();
+
+ }
+ $it->next();
+
+ }
+ unset($this->children[$key]);
+
+ }
+
+ foreach($newEvents as $newEvent) {
+
+ foreach($newEvent->children as $child) {
+ if ($child instanceof VObject\Property\DateTime &&
+ $child->getDateType() == VObject\Property\DateTime::LOCALTZ) {
+ $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC);
+ }
+ }
+
+ $this->add($newEvent);
+
+ }
+
+ // Removing all VTIMEZONE components
+ unset($this->VTIMEZONE);
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @return array
+ */
+ /*
+ public function validate() {
+
+ $warnings = array();
+
+ $version = $this->select('VERSION');
+ if (count($version)!==1) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time',
+ 'node' => $this,
+ );
+ } else {
+ if ((string)$this->VERSION !== '2.0') {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
+ 'node' => $this,
+ );
+ }
+ }
+ $version = $this->select('PRODID');
+ if (count($version)!==1) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time',
+ 'node' => $this,
+ );
+ }
+ if (count($this->CALSCALE) > 1) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The CALSCALE property must not be specified more than once.',
+ 'node' => $this,
+ );
+ }
+ if (count($this->METHOD) > 1) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The METHOD property must not be specified more than once.',
+ 'node' => $this,
+ );
+ }
+
+ $allowedComponents = array(
+ 'VEVENT',
+ 'VTODO',
+ 'VJOURNAL',
+ 'VFREEBUSY',
+ 'VTIMEZONE',
+ );
+ $allowedProperties = array(
+ 'PRODID',
+ 'VERSION',
+ 'CALSCALE',
+ 'METHOD',
+ );
+ $componentsFound = 0;
+ foreach($this->children as $child) {
+ if($child instanceof Component) {
+ $componentsFound++;
+ if (!in_array($child->name, $allowedComponents)) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component",
+ 'node' => $this,
+ );
+ }
+ }
+ if ($child instanceof Property) {
+ if (!in_array($child->name, $allowedProperties)) {
+ $warnings[] = array(
+ 'level' => 2,
+ 'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component",
+ 'node' => $this,
+ );
+ }
+ }
+ }
+
+ if ($componentsFound===0) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'An iCalendar object must have at least 1 component.',
+ 'node' => $this,
+ );
+ }
+
+ return array_merge(
+ $warnings,
+ parent::validate()
+ );
+
+ }
+ */
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php
new file mode 100644
index 0000000..8a50f38
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VCard.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VCard component
+ *
+ * This component represents the BEGIN:VCARD and END:VCARD found in every
+ * vcard.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCard extends VObject\Component {
+
+ static $defaultName = 'VCARD';
+
+ /**
+ * VCards with version 2.1, 3.0 and 4.0 are found.
+ *
+ * If the VCARD doesn't know its version, 4.0 is assumed.
+ */
+ const DEFAULT_VERSION = '4.0';
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $warnings = array();
+
+ $version = $this->select('VERSION');
+ if (count($version)!==1) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The VERSION property must appear in the VCARD component exactly 1 time',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ $this->VERSION = self::DEFAULT_VERSION;
+ }
+ } else {
+ $version = (string)$this->VERSION;
+ if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ $this->VERSION = '4.0';
+ }
+ }
+
+ }
+ $fn = $this->select('FN');
+ if (count($fn)!==1) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The FN property must appear in the VCARD component exactly 1 time',
+ 'node' => $this,
+ );
+ if (($options & self::REPAIR) && count($fn) === 0) {
+ // We're going to try to see if we can use the contents of the
+ // N property.
+ if (isset($this->N)) {
+ $value = explode(';', (string)$this->N);
+ if (isset($value[1]) && $value[1]) {
+ $this->FN = $value[1] . ' ' . $value[0];
+ } else {
+ $this->FN = $value[0];
+ }
+
+ // Otherwise, the ORG property may work
+ } elseif (isset($this->ORG)) {
+ $this->FN = (string)$this->ORG;
+ }
+
+ }
+ }
+
+ return array_merge(
+ parent::validate($options),
+ $warnings
+ );
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php
new file mode 100644
index 0000000..a8af08f
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VEvent.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+use OldSabre\VObject;
+
+/**
+ * VEvent component
+ *
+ * This component contains some additional functionality specific for VEVENT's.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VEvent extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param \DateTime $start
+ * @param \DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ if ($this->RRULE) {
+ $it = new VObject\RecurrenceIterator($this);
+ $it->fastForward($start);
+
+ // We fast-forwarded to a spot where the end-time of the
+ // recurrence instance exceeded the start of the requested
+ // time-range.
+ //
+ // If the starttime of the recurrence did not exceed the
+ // end of the time range as well, we have a match.
+ return ($it->getDTStart() < $end && $it->getDTEnd() > $start);
+
+ }
+
+ $effectiveStart = $this->DTSTART->getDateTime();
+ if (isset($this->DTEND)) {
+
+ // The DTEND property is considered non inclusive. So for a 3 day
+ // event in july, dtstart and dtend would have to be July 1st and
+ // July 4th respectively.
+ //
+ // See:
+ // http://tools.ietf.org/html/rfc5545#page-54
+ $effectiveEnd = $this->DTEND->getDateTime();
+
+ } elseif (isset($this->DURATION)) {
+ $effectiveEnd = clone $effectiveStart;
+ $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) );
+ } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+ $effectiveEnd = clone $effectiveStart;
+ $effectiveEnd->modify('+1 day');
+ } else {
+ $effectiveEnd = clone $effectiveStart;
+ }
+ return (
+ ($start <= $effectiveEnd) && ($end > $effectiveStart)
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php
new file mode 100644
index 0000000..abb622b
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VFreeBusy.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * The VFreeBusy component
+ *
+ * This component adds functionality to a component, specific for VFREEBUSY
+ * components.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VFreeBusy extends VObject\Component {
+
+ /**
+ * Checks based on the contained FREEBUSY information, if a timeslot is
+ * available.
+ *
+ * @param DateTime $start
+ * @param Datetime $end
+ * @return bool
+ */
+ public function isFree(\DateTime $start, \Datetime $end) {
+
+ foreach($this->select('FREEBUSY') as $freebusy) {
+
+ // We are only interested in FBTYPE=BUSY (the default),
+ // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
+ if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') {
+ continue;
+ }
+
+ // The freebusy component can hold more than 1 value, separated by
+ // commas.
+ $periods = explode(',', (string)$freebusy);
+
+ foreach($periods as $period) {
+ // Every period is formatted as [start]/[end]. The start is an
+ // absolute UTC time, the end may be an absolute UTC time, or
+ // duration (relative) value.
+ list($busyStart, $busyEnd) = explode('/', $period);
+
+ $busyStart = VObject\DateTimeParser::parse($busyStart);
+ $busyEnd = VObject\DateTimeParser::parse($busyEnd);
+ if ($busyEnd instanceof \DateInterval) {
+ $tmp = clone $busyStart;
+ $tmp->add($busyEnd);
+ $busyEnd = $tmp;
+ }
+
+ if($start < $busyEnd && $end > $busyStart) {
+ return false;
+ }
+
+ }
+
+ }
+
+ return true;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php
new file mode 100644
index 0000000..25f1593
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VJournal.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * VJournal component
+ *
+ * This component contains some additional functionality specific for VJOURNALs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VJournal extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+ if ($dtstart) {
+ $effectiveEnd = clone $dtstart;
+ if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+ $effectiveEnd->modify('+1 day');
+ }
+
+ return ($start <= $effectiveEnd && $end > $dtstart);
+
+ }
+ return false;
+
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php
new file mode 100644
index 0000000..c1a3eee
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Component/VTodo.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace OldSabre\VObject\Component;
+
+use OldSabre\VObject;
+
+/**
+ * VTodo component
+ *
+ * This component contains some additional functionality specific for VTODOs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VTodo extends VObject\Component {
+
+ /**
+ * Returns true or false depending on if the event falls in the specified
+ * time-range. This is used for filtering purposes.
+ *
+ * The rules used to determine if an event falls within the specified
+ * time-range is based on the CalDAV specification.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return bool
+ */
+ public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+ $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+ $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null;
+ $due = isset($this->DUE)?$this->DUE->getDateTime():null;
+ $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null;
+ $created = isset($this->CREATED)?$this->CREATED->getDateTime():null;
+
+ if ($dtstart) {
+ if ($duration) {
+ $effectiveEnd = clone $dtstart;
+ $effectiveEnd->add($duration);
+ return $start <= $effectiveEnd && $end > $dtstart;
+ } elseif ($due) {
+ return
+ ($start < $due || $start <= $dtstart) &&
+ ($end > $dtstart || $end >= $due);
+ } else {
+ return $start <= $dtstart && $end > $dtstart;
+ }
+ }
+ if ($due) {
+ return ($start < $due && $end >= $due);
+ }
+ if ($completed && $created) {
+ return
+ ($start <= $created || $start <= $completed) &&
+ ($end >= $created || $end >= $completed);
+ }
+ if ($completed) {
+ return ($start <= $completed && $end >= $completed);
+ }
+ if ($created) {
+ return ($end > $created);
+ }
+ return true;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php
new file mode 100644
index 0000000..1b388fa
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/DateTimeParser.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * DateTimeParser
+ *
+ * This class is responsible for parsing the several different date and time
+ * formats iCalendar and vCards have.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class DateTimeParser {
+
+ /**
+ * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object
+ *
+ * Specifying a reference timezone is optional. It will only be used
+ * if the non-UTC format is used. The argument is used as a reference, the
+ * returned DateTime object will still be in the UTC timezone.
+ *
+ * @param string $dt
+ * @param DateTimeZone $tz
+ * @return DateTime
+ */
+ static public function parseDateTime($dt,\DateTimeZone $tz = null) {
+
+ // Format is YYYYMMDD + "T" + hhmmss
+ $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
+
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
+ }
+
+ if ($matches[7]==='Z' || is_null($tz)) {
+ $tz = new \DateTimeZone('UTC');
+ }
+ $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz);
+
+ // Still resetting the timezone, to normalize everything to UTC
+ $date->setTimeZone(new \DateTimeZone('UTC'));
+ return $date;
+
+ }
+
+ /**
+ * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object
+ *
+ * @param string $date
+ * @return DateTime
+ */
+ static public function parseDate($date) {
+
+ // Format is YYYYMMDD
+ $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
+
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date);
+ }
+
+ $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC'));
+ return $date;
+
+ }
+
+ /**
+ * Parses an iCalendar (RFC5545) formatted duration value.
+ *
+ * This method will either return a DateTimeInterval object, or a string
+ * suitable for strtotime or DateTime::modify.
+ *
+ * @param string $duration
+ * @param bool $asString
+ * @return DateInterval|string
+ */
+ static public function parseDuration($duration, $asString = false) {
+
+ $result = preg_match('/^(?P<plusminus>\+|-)?P((?P<week>\d+)W)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+)S)?)?$/', $duration, $matches);
+ if (!$result) {
+ throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration);
+ }
+
+ if (!$asString) {
+ $invert = false;
+ if ($matches['plusminus']==='-') {
+ $invert = true;
+ }
+
+
+ $parts = array(
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ );
+ foreach($parts as $part) {
+ $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0;
+ }
+
+
+ // We need to re-construct the $duration string, because weeks and
+ // days are not supported by DateInterval in the same string.
+ $duration = 'P';
+ $days = $matches['day'];
+ if ($matches['week']) {
+ $days+=$matches['week']*7;
+ }
+ if ($days)
+ $duration.=$days . 'D';
+
+ if ($matches['minute'] || $matches['second'] || $matches['hour']) {
+ $duration.='T';
+
+ if ($matches['hour'])
+ $duration.=$matches['hour'].'H';
+
+ if ($matches['minute'])
+ $duration.=$matches['minute'].'M';
+
+ if ($matches['second'])
+ $duration.=$matches['second'].'S';
+
+ }
+
+ if ($duration==='P') {
+ $duration = 'PT0S';
+ }
+ $iv = new \DateInterval($duration);
+ if ($invert) $iv->invert = true;
+
+ return $iv;
+
+ }
+
+
+
+ $parts = array(
+ 'week',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ );
+
+ $newDur = '';
+ foreach($parts as $part) {
+ if (isset($matches[$part]) && $matches[$part]) {
+ $newDur.=' '.$matches[$part] . ' ' . $part . 's';
+ }
+ }
+
+ $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur);
+ if ($newDur === '+') { $newDur = '+0 seconds'; };
+ return $newDur;
+
+ }
+
+ /**
+ * Parses either a Date or DateTime, or Duration value.
+ *
+ * @param string $date
+ * @param DateTimeZone|string $referenceTZ
+ * @return DateTime|DateInterval
+ */
+ static public function parse($date, $referenceTZ = null) {
+
+ if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) {
+ return self::parseDuration($date);
+ } elseif (strlen($date)===8) {
+ return self::parseDate($date);
+ } else {
+ return self::parseDateTime($date, $referenceTZ);
+ }
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php
new file mode 100644
index 0000000..b29d971
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Document.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Document
+ *
+ * A document is just like a component, except that it's also the top level
+ * element.
+ *
+ * Both a VCALENDAR and a VCARD are considered documents.
+ *
+ * This class also provides a registry for document types.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+abstract class Document extends Component {
+
+ /**
+ * The default name for this component.
+ *
+ * This should be 'VCALENDAR' or 'VCARD'.
+ *
+ * @var string
+ */
+ static $defaultName;
+
+ /**
+ * Creates a new document.
+ *
+ * We're changing the default behavior slightly here. First, we don't want
+ * to have to specify a name (we already know it), and we want to allow
+ * children to be specified in the first argument.
+ *
+ * But, the default behavior also works.
+ *
+ * So the two sigs:
+ *
+ * new Document(array $children = array());
+ * new Document(string $name, array $children = array())
+ *
+ * @return void
+ */
+ public function __construct() {
+
+ $args = func_get_args();
+ if (count($args)===0 || is_array($args[0])) {
+ array_unshift($args, static::$defaultName);
+ call_user_func_array(array('parent', '__construct'), $args);
+ } else {
+ call_user_func_array(array('parent', '__construct'), $args);
+ }
+
+ }
+
+ /**
+ * Creates a new component
+ *
+ * This method automatically searches for the correct component class, based
+ * on its name.
+ *
+ * You can specify the children either in key=>value syntax, in which case
+ * properties will automatically be created, or you can just pass a list of
+ * Component and Property object.
+ *
+ * @param string $name
+ * @param array $children
+ * @return Component
+ */
+ public function createComponent($name, array $children = array()) {
+
+ $component = Component::create($name);
+ foreach($children as $k=>$v) {
+
+ if ($v instanceof Node) {
+ $component->add($v);
+ } else {
+ $component->add($k, $v);
+ }
+
+ }
+ return $component;
+
+ }
+
+ /**
+ * Factory method for creating new properties
+ *
+ * This method automatically searches for the correct property class, based
+ * on its name.
+ *
+ * You can specify the parameters either in key=>value syntax, in which case
+ * parameters will automatically be created, or you can just pass a list of
+ * Parameter objects.
+ *
+ * @param string $name
+ * @param mixed $value
+ * @param array $parameters
+ * @return Property
+ */
+ public function createProperty($name, $value = null, array $parameters = array()) {
+
+ return Property::create($name, $value, $parameters);
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php
new file mode 100644
index 0000000..317d784
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ElementList.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject ElementList
+ *
+ * This class represents a list of elements. Lists are the result of queries,
+ * such as doing $vcalendar->vevent where there's multiple VEVENT objects.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ElementList implements \Iterator, \Countable, \ArrayAccess {
+
+ /**
+ * Inner elements
+ *
+ * @var array
+ */
+ protected $elements = array();
+
+ /**
+ * Creates the element list.
+ *
+ * @param array $elements
+ */
+ public function __construct(array $elements) {
+
+ $this->elements = $elements;
+
+ }
+
+ /* {{{ Iterator interface */
+
+ /**
+ * Current position
+ *
+ * @var int
+ */
+ private $key = 0;
+
+ /**
+ * Returns current item in iteration
+ *
+ * @return Element
+ */
+ public function current() {
+
+ return $this->elements[$this->key];
+
+ }
+
+ /**
+ * To the next item in the iterator
+ *
+ * @return void
+ */
+ public function next() {
+
+ $this->key++;
+
+ }
+
+ /**
+ * Returns the current iterator key
+ *
+ * @return int
+ */
+ public function key() {
+
+ return $this->key;
+
+ }
+
+ /**
+ * Returns true if the current position in the iterator is a valid one
+ *
+ * @return bool
+ */
+ public function valid() {
+
+ return isset($this->elements[$this->key]);
+
+ }
+
+ /**
+ * Rewinds the iterator
+ *
+ * @return void
+ */
+ public function rewind() {
+
+ $this->key = 0;
+
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements
+ *
+ * @return int
+ */
+ public function count() {
+
+ return count($this->elements);
+
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * @param int $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+
+ return isset($this->elements[$offset]);
+
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+
+ return $this->elements[$offset];
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * @param int $offset
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($offset,$value) {
+
+ throw new \LogicException('You can not add new objects to an ElementList');
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return void
+ */
+ public function offsetUnset($offset) {
+
+ throw new \LogicException('You can not remove objects from an ElementList');
+
+ }
+
+ /* }}} */
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php
new file mode 100644
index 0000000..a7a8ef3
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/FreeBusyGenerator.php
@@ -0,0 +1,322 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class helps with generating FREEBUSY reports based on existing sets of
+ * objects.
+ *
+ * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
+ * generates a single VFREEBUSY object.
+ *
+ * VFREEBUSY components are described in RFC5545, The rules for what should
+ * go in a single freebusy report is taken from RFC4791, section 7.10.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class FreeBusyGenerator {
+
+ /**
+ * Input objects
+ *
+ * @var array
+ */
+ protected $objects;
+
+ /**
+ * Start of range
+ *
+ * @var DateTime|null
+ */
+ protected $start;
+
+ /**
+ * End of range
+ *
+ * @var DateTime|null
+ */
+ protected $end;
+
+ /**
+ * VCALENDAR object
+ *
+ * @var Component
+ */
+ protected $baseObject;
+
+ /**
+ * Creates the generator.
+ *
+ * Check the setTimeRange and setObjects methods for details about the
+ * arguments.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @param mixed $objects
+ * @return void
+ */
+ public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null) {
+
+ if ($start && $end) {
+ $this->setTimeRange($start, $end);
+ }
+
+ if ($objects) {
+ $this->setObjects($objects);
+ }
+
+ }
+
+ /**
+ * Sets the VCALENDAR object.
+ *
+ * If this is set, it will not be generated for you. You are responsible
+ * for setting things like the METHOD, CALSCALE, VERSION, etc..
+ *
+ * The VFREEBUSY object will be automatically added though.
+ *
+ * @param Component $vcalendar
+ * @return void
+ */
+ public function setBaseObject(Component $vcalendar) {
+
+ $this->baseObject = $vcalendar;
+
+ }
+
+ /**
+ * Sets the input objects
+ *
+ * You must either specify a valendar object as a strong, or as the parse
+ * Component.
+ * It's also possible to specify multiple objects as an array.
+ *
+ * @param mixed $objects
+ * @return void
+ */
+ public function setObjects($objects) {
+
+ if (!is_array($objects)) {
+ $objects = array($objects);
+ }
+
+ $this->objects = array();
+ foreach($objects as $object) {
+
+ if (is_string($object)) {
+ $this->objects[] = Reader::read($object);
+ } elseif ($object instanceof Component) {
+ $this->objects[] = $object;
+ } else {
+ throw new \InvalidArgumentException('You can only pass strings or \\OldSabre\\VObject\\Component arguments to setObjects');
+ }
+
+ }
+
+ }
+
+ /**
+ * Sets the time range
+ *
+ * Any freebusy object falling outside of this time range will be ignored.
+ *
+ * @param DateTime $start
+ * @param DateTime $end
+ * @return void
+ */
+ public function setTimeRange(\DateTime $start = null, \DateTime $end = null) {
+
+ $this->start = $start;
+ $this->end = $end;
+
+ }
+
+ /**
+ * Parses the input data and returns a correct VFREEBUSY object, wrapped in
+ * a VCALENDAR.
+ *
+ * @return Component
+ */
+ public function getResult() {
+
+ $busyTimes = array();
+
+ foreach($this->objects as $object) {
+
+ foreach($object->getBaseComponents() as $component) {
+
+ switch($component->name) {
+
+ case 'VEVENT' :
+
+ $FBTYPE = 'BUSY';
+ if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) {
+ break;
+ }
+ if (isset($component->STATUS)) {
+ $status = strtoupper($component->STATUS);
+ if ($status==='CANCELLED') {
+ break;
+ }
+ if ($status==='TENTATIVE') {
+ $FBTYPE = 'BUSY-TENTATIVE';
+ }
+ }
+
+ $times = array();
+
+ if ($component->RRULE) {
+
+ $iterator = new RecurrenceIterator($object, (string)$component->uid);
+ if ($this->start) {
+ $iterator->fastForward($this->start);
+ }
+
+ $maxRecurrences = 200;
+
+ while($iterator->valid() && --$maxRecurrences) {
+
+ $startTime = $iterator->getDTStart();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $times[] = array(
+ $iterator->getDTStart(),
+ $iterator->getDTEnd(),
+ );
+
+ $iterator->next();
+
+ }
+
+ } else {
+
+ $startTime = $component->DTSTART->getDateTime();
+ if ($this->end && $startTime > $this->end) {
+ break;
+ }
+ $endTime = null;
+ if (isset($component->DTEND)) {
+ $endTime = $component->DTEND->getDateTime();
+ } elseif (isset($component->DURATION)) {
+ $duration = DateTimeParser::parseDuration((string)$component->DURATION);
+ $endTime = clone $startTime;
+ $endTime->add($duration);
+ } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) {
+ $endTime = clone $startTime;
+ $endTime->modify('+1 day');
+ } else {
+ // The event had no duration (0 seconds)
+ break;
+ }
+
+ $times[] = array($startTime, $endTime);
+
+ }
+
+ foreach($times as $time) {
+
+ if ($this->end && $time[0] > $this->end) break;
+ if ($this->start && $time[1] < $this->start) break;
+
+ $busyTimes[] = array(
+ $time[0],
+ $time[1],
+ $FBTYPE,
+ );
+ }
+ break;
+
+ case 'VFREEBUSY' :
+ foreach($component->FREEBUSY as $freebusy) {
+
+ $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY';
+
+ // Skipping intervals marked as 'free'
+ if ($fbType==='FREE')
+ continue;
+
+ $values = explode(',', $freebusy);
+ foreach($values as $value) {
+ list($startTime, $endTime) = explode('/', $value);
+ $startTime = DateTimeParser::parseDateTime($startTime);
+
+ if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') {
+ $duration = DateTimeParser::parseDuration($endTime);
+ $endTime = clone $startTime;
+ $endTime->add($duration);
+ } else {
+ $endTime = DateTimeParser::parseDateTime($endTime);
+ }
+
+ if($this->start && $this->start > $endTime) continue;
+ if($this->end && $this->end < $startTime) continue;
+ $busyTimes[] = array(
+ $startTime,
+ $endTime,
+ $fbType
+ );
+
+ }
+
+
+ }
+ break;
+
+
+
+ }
+
+
+ }
+
+ }
+
+ if ($this->baseObject) {
+ $calendar = $this->baseObject;
+ } else {
+ $calendar = Component::create('VCALENDAR');
+ $calendar->version = '2.0';
+ $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN';
+ $calendar->calscale = 'GREGORIAN';
+ }
+
+ $vfreebusy = Component::create('VFREEBUSY');
+ $calendar->add($vfreebusy);
+
+ if ($this->start) {
+ $dtstart = Property::create('DTSTART');
+ $dtstart->setDateTime($this->start,Property\DateTime::UTC);
+ $vfreebusy->add($dtstart);
+ }
+ if ($this->end) {
+ $dtend = Property::create('DTEND');
+ $dtend->setDateTime($this->end,Property\DateTime::UTC);
+ $vfreebusy->add($dtend);
+ }
+ $dtstamp = Property::create('DTSTAMP');
+ $dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC);
+ $vfreebusy->add($dtstamp);
+
+ foreach($busyTimes as $busyTime) {
+
+ $busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
+ $busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
+
+ $prop = Property::create(
+ 'FREEBUSY',
+ $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
+ );
+ $prop['FBTYPE'] = $busyTime[2];
+ $vfreebusy->add($prop);
+
+ }
+
+ return $calendar;
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php
new file mode 100644
index 0000000..b9e265b
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Node.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Base class for all nodes
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
+
+ /**
+ * The following constants are used by the validate() method.
+ */
+ const REPAIR = 1;
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ abstract function serialize();
+
+ /**
+ * Iterator override
+ *
+ * @var ElementList
+ */
+ protected $iterator = null;
+
+ /**
+ * A link to the parent node
+ *
+ * @var Node
+ */
+ public $parent = null;
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ return array();
+
+ }
+
+ /* {{{ IteratorAggregator interface */
+
+ /**
+ * Returns the iterator for this object
+ *
+ * @return ElementList
+ */
+ public function getIterator() {
+
+ if (!is_null($this->iterator))
+ return $this->iterator;
+
+ return new ElementList(array($this));
+
+ }
+
+ /**
+ * Sets the overridden iterator
+ *
+ * Note that this is not actually part of the iterator interface
+ *
+ * @param ElementList $iterator
+ * @return void
+ */
+ public function setIterator(ElementList $iterator) {
+
+ $this->iterator = $iterator;
+
+ }
+
+ /* }}} */
+
+ /* {{{ Countable interface */
+
+ /**
+ * Returns the number of elements
+ *
+ * @return int
+ */
+ public function count() {
+
+ $it = $this->getIterator();
+ return $it->count();
+
+ }
+
+ /* }}} */
+
+ /* {{{ ArrayAccess Interface */
+
+
+ /**
+ * Checks if an item exists through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return bool
+ */
+ public function offsetExists($offset) {
+
+ $iterator = $this->getIterator();
+ return $iterator->offsetExists($offset);
+
+ }
+
+ /**
+ * Gets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return mixed
+ */
+ public function offsetGet($offset) {
+
+ $iterator = $this->getIterator();
+ return $iterator->offsetGet($offset);
+
+ }
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($offset,$value) {
+
+ $iterator = $this->getIterator();
+ $iterator->offsetSet($offset,$value);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+ // @codeCoverageIgnoreEnd
+
+ /**
+ * Sets an item through ArrayAccess.
+ *
+ * This method just forwards the request to the inner iterator
+ *
+ * @param int $offset
+ * @return void
+ */
+ public function offsetUnset($offset) {
+
+ $iterator = $this->getIterator();
+ $iterator->offsetUnset($offset);
+
+ // @codeCoverageIgnoreStart
+ //
+ // This method always throws an exception, so we ignore the closing
+ // brace
+ }
+ // @codeCoverageIgnoreEnd
+
+ /* }}} */
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php
new file mode 100644
index 0000000..43d2433
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Parameter.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Parameter
+ *
+ * This class represents a parameter. A parameter is always tied to a property.
+ * In the case of:
+ * DTSTART;VALUE=DATE:20101108
+ * VALUE=DATE would be the parameter name and value.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Parameter extends Node {
+
+ /**
+ * Parameter name
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Parameter value
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * Sets up the object
+ *
+ * @param string $name
+ * @param string $value
+ */
+ public function __construct($name, $value = null) {
+
+ if (!is_scalar($value) && !is_null($value)) {
+ throw new \InvalidArgumentException('The value argument must be a scalar value or null');
+ }
+
+ $this->name = strtoupper($name);
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the parameter's internal value.
+ *
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ if (is_null($this->value)) {
+ return $this->name;
+ }
+ $src = array(
+ '\\',
+ "\n",
+ ';',
+ ',',
+ );
+ $out = array(
+ '\\\\',
+ '\n',
+ '\;',
+ '\,',
+ );
+
+ return $this->name . '=' . str_replace($src, $out, $this->value);
+
+ }
+
+ /**
+ * Called when this object is being cast to a string
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ return $this->value;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php
new file mode 100644
index 0000000..8e24d78
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/ParseException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Exception thrown by Reader if an invalid object was attempted to be parsed.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ParseException extends \Exception { }
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php
new file mode 100644
index 0000000..ad99f59
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property.php
@@ -0,0 +1,442 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VObject Property
+ *
+ * A property in VObject is usually in the form PARAMNAME:paramValue.
+ * An example is : SUMMARY:Weekly meeting
+ *
+ * Properties can also have parameters:
+ * SUMMARY;LANG=en:Weekly meeting.
+ *
+ * Parameters can be accessed using the ArrayAccess interface.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Property extends Node {
+
+ /**
+ * Propertyname
+ *
+ * @var string
+ */
+ public $name;
+
+ /**
+ * Group name
+ *
+ * This may be something like 'HOME' for vcards.
+ *
+ * @var string
+ */
+ public $group;
+
+ /**
+ * Property parameters
+ *
+ * @var array
+ */
+ public $parameters = array();
+
+ /**
+ * Property value
+ *
+ * @var string
+ */
+ public $value;
+
+ /**
+ * If properties are added to this map, they will be automatically mapped
+ * to their respective classes, if parsed by the reader or constructed with
+ * the 'create' method.
+ *
+ * @var array
+ */
+ static public $classMap = array(
+ 'COMPLETED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'CREATED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTEND' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTSTAMP' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DTSTART' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'DUE' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'EXDATE' => 'OldSabre\\VObject\\Property\\MultiDateTime',
+ 'LAST-MODIFIED' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'RECURRENCE-ID' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'TRIGGER' => 'OldSabre\\VObject\\Property\\DateTime',
+ 'N' => 'OldSabre\\VObject\\Property\\Compound',
+ 'ORG' => 'OldSabre\\VObject\\Property\\Compound',
+ 'ADR' => 'OldSabre\\VObject\\Property\\Compound',
+ 'CATEGORIES' => 'OldSabre\\VObject\\Property\\Compound',
+ );
+
+ /**
+ * Creates the new property by name, but in addition will also see if
+ * there's a class mapped to the property name.
+ *
+ * Parameters can be specified with the optional third argument. Parameters
+ * must be a key->value map of the parameter name, and value. If the value
+ * is specified as an array, it is assumed that multiple parameters with
+ * the same name should be added.
+ *
+ * @param string $name
+ * @param string $value
+ * @param array $parameters
+ * @return Property
+ */
+ static public function create($name, $value = null, array $parameters = array()) {
+
+ $name = strtoupper($name);
+ $shortName = $name;
+ $group = null;
+ if (strpos($shortName,'.')!==false) {
+ list($group, $shortName) = explode('.', $shortName);
+ }
+
+ if (isset(self::$classMap[$shortName])) {
+ return new self::$classMap[$shortName]($name, $value, $parameters);
+ } else {
+ return new self($name, $value, $parameters);
+ }
+
+ }
+
+ /**
+ * Creates a new property object
+ *
+ * Parameters can be specified with the optional third argument. Parameters
+ * must be a key->value map of the parameter name, and value. If the value
+ * is specified as an array, it is assumed that multiple parameters with
+ * the same name should be added.
+ *
+ * @param string $name
+ * @param string $value
+ * @param array $parameters
+ */
+ public function __construct($name, $value = null, array $parameters = array()) {
+
+ if (!is_scalar($value) && !is_null($value)) {
+ throw new \InvalidArgumentException('The value argument must be scalar or null');
+ }
+
+ $name = strtoupper($name);
+ $group = null;
+ if (strpos($name,'.')!==false) {
+ list($group, $name) = explode('.', $name);
+ }
+ $this->name = $name;
+ $this->group = $group;
+ $this->setValue($value);
+
+ foreach($parameters as $paramName => $paramValues) {
+
+ if (!is_array($paramValues)) {
+ $paramValues = array($paramValues);
+ }
+
+ foreach($paramValues as $paramValue) {
+ $this->add($paramName, $paramValue);
+ }
+
+ }
+
+ }
+
+ /**
+ * Updates the internal value
+ *
+ * @param string $value
+ * @return void
+ */
+ public function setValue($value) {
+
+ $this->value = $value;
+
+ }
+
+ /**
+ * Returns the internal value
+ *
+ * @param string $value
+ * @return string
+ */
+ public function getValue() {
+
+ return $this->value;
+
+ }
+
+ /**
+ * Turns the object back into a serialized blob.
+ *
+ * @return string
+ */
+ public function serialize() {
+
+ $str = $this->name;
+ if ($this->group) $str = $this->group . '.' . $this->name;
+
+ foreach($this->parameters as $param) {
+
+ $str.=';' . $param->serialize();
+
+ }
+
+ $src = array(
+ '\\',
+ "\n",
+ );
+ $out = array(
+ '\\\\',
+ '\n',
+ );
+ $str.=':' . str_replace($src, $out, $this->value);
+
+ $out = '';
+ while(strlen($str)>0) {
+ if (strlen($str)>75) {
+ $out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
+ $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
+ } else {
+ $out.=$str . "\r\n";
+ $str='';
+ break;
+ }
+ }
+
+ return $out;
+
+ }
+
+ /**
+ * Adds a new componenten or element
+ *
+ * You can call this method with the following syntaxes:
+ *
+ * add(Parameter $element)
+ * add(string $name, $value)
+ *
+ * The first version adds an Parameter
+ * The second adds a property as a string.
+ *
+ * @param mixed $item
+ * @param mixed $itemValue
+ * @return void
+ */
+ public function add($item, $itemValue = null) {
+
+ if ($item instanceof Parameter) {
+ if (!is_null($itemValue)) {
+ throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject');
+ }
+ $item->parent = $this;
+ $this->parameters[] = $item;
+ } elseif(is_string($item)) {
+
+ $parameter = new Parameter($item,$itemValue);
+ $parameter->parent = $this;
+ $this->parameters[] = $parameter;
+
+ } else {
+
+ throw new \InvalidArgumentException('The first argument must either be a Node a string');
+
+ }
+
+ }
+
+ /* ArrayAccess interface {{{ */
+
+ /**
+ * Checks if an array element exists
+ *
+ * @param mixed $name
+ * @return bool
+ */
+ public function offsetExists($name) {
+
+ if (is_int($name)) return parent::offsetExists($name);
+
+ $name = strtoupper($name);
+
+ foreach($this->parameters as $parameter) {
+ if ($parameter->name == $name) return true;
+ }
+ return false;
+
+ }
+
+ /**
+ * Returns a parameter, or parameter list.
+ *
+ * @param string $name
+ * @return Node
+ */
+ public function offsetGet($name) {
+
+ if (is_int($name)) return parent::offsetGet($name);
+ $name = strtoupper($name);
+
+ $result = array();
+ foreach($this->parameters as $parameter) {
+ if ($parameter->name == $name)
+ $result[] = $parameter;
+ }
+
+ if (count($result)===0) {
+ return null;
+ } elseif (count($result)===1) {
+ return $result[0];
+ } else {
+ $result[0]->setIterator(new ElementList($result));
+ return $result[0];
+ }
+
+ }
+
+ /**
+ * Creates a new parameter
+ *
+ * @param string $name
+ * @param mixed $value
+ * @return void
+ */
+ public function offsetSet($name, $value) {
+
+ if (is_int($name)) parent::offsetSet($name, $value);
+
+ if (is_scalar($value)) {
+ if (!is_string($name))
+ throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.');
+
+ $this->offsetUnset($name);
+ $parameter = new Parameter($name, $value);
+ $parameter->parent = $this;
+ $this->parameters[] = $parameter;
+
+ } elseif ($value instanceof Parameter) {
+ if (!is_null($name))
+ throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\OldSabre\\VObject\\Parameter. Add using $array[]=$parameterObject.');
+
+ $value->parent = $this;
+ $this->parameters[] = $value;
+ } else {
+ throw new \InvalidArgumentException('You can only add parameters to the property object');
+ }
+
+ }
+
+ /**
+ * Removes one or more parameters with the specified name
+ *
+ * @param string $name
+ * @return void
+ */
+ public function offsetUnset($name) {
+
+ if (is_int($name)) parent::offsetUnset($name);
+ $name = strtoupper($name);
+
+ foreach($this->parameters as $key=>$parameter) {
+ if ($parameter->name == $name) {
+ $parameter->parent = null;
+ unset($this->parameters[$key]);
+ }
+
+ }
+
+ }
+
+ /* }}} */
+
+ /**
+ * Called when this object is being cast to a string
+ *
+ * @return string
+ */
+ public function __toString() {
+
+ return (string)$this->value;
+
+ }
+
+ /**
+ * This method is automatically called when the object is cloned.
+ * Specifically, this will ensure all child elements are also cloned.
+ *
+ * @return void
+ */
+ public function __clone() {
+
+ foreach($this->parameters as $key=>$child) {
+ $this->parameters[$key] = clone $child;
+ $this->parameters[$key]->parent = $this;
+ }
+
+ }
+
+ /**
+ * Validates the node for correctness.
+ *
+ * The following options are supported:
+ * - Node::REPAIR - If something is broken, and automatic repair may
+ * be attempted.
+ *
+ * An array is returned with warnings.
+ *
+ * Every item in the array has the following properties:
+ * * level - (number between 1 and 3 with severity information)
+ * * message - (human readable message)
+ * * node - (reference to the offending node)
+ *
+ * @param int $options
+ * @return array
+ */
+ public function validate($options = 0) {
+
+ $warnings = array();
+
+ // Checking if our value is UTF-8
+ if (!StringUtil::isUTF8($this->value)) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'Property is not valid UTF-8!',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ $this->value = StringUtil::convertToUTF8($this->value);
+ }
+ }
+
+ // Checking if the propertyname does not contain any invalid bytes.
+ if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
+ $warnings[] = array(
+ 'level' => 1,
+ 'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed',
+ 'node' => $this,
+ );
+ if ($options & self::REPAIR) {
+ // Uppercasing and converting underscores to dashes.
+ $this->name = strtoupper(
+ str_replace('_', '-', $this->name)
+ );
+ // Removing every other invalid character
+ $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
+
+ }
+
+ }
+
+ // Validating inner parameters
+ foreach($this->parameters as $param) {
+ $warnings = array_merge($warnings, $param->validate($options));
+ }
+
+ return $warnings;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php
new file mode 100644
index 0000000..e46946e
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/Compound.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * Compound property.
+ *
+ * This class adds (de)serialization of compound properties to/from arrays.
+ *
+ * Currently the following properties from RFC 6350 are mapped to use this
+ * class:
+ *
+ * N: Section 6.2.2
+ * ADR: Section 6.3.1
+ * ORG: Section 6.6.4
+ * CATEGORIES: Section 6.7.1
+ *
+ * In order to use this correctly, you must call setParts and getParts to
+ * retrieve and modify dates respectively.
+ *
+ * @author Thomas Tanghus (http://tanghus.net/)
+ * @author Lars Kneschke
+ * @author Evert Pot (http://evertpot.com/)
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Compound extends VObject\Property {
+
+ /**
+ * If property names are added to this map, they will be (de)serialised as arrays
+ * using the getParts() and setParts() methods.
+ * The keys are the property names, values are delimiter chars.
+ *
+ * @var array
+ */
+ static public $delimiterMap = array(
+ 'N' => ';',
+ 'ADR' => ';',
+ 'ORG' => ';',
+ 'CATEGORIES' => ',',
+ );
+
+ /**
+ * The currently used delimiter.
+ *
+ * @var string
+ */
+ protected $delimiter = null;
+
+ /**
+ * Get a compound value as an array.
+ *
+ * @param $name string
+ * @return array
+ */
+ public function getParts() {
+
+ if (is_null($this->value)) {
+ return array();
+ }
+
+ $delimiter = $this->getDelimiter();
+
+ // split by any $delimiter which is NOT prefixed by a slash.
+ // Note that this is not a a perfect solution. If a value is prefixed
+ // by two slashes, it should actually be split anyway.
+ //
+ // Hopefully we can fix this better in a future version, where we can
+ // break compatibility a bit.
+ $compoundValues = preg_split("/(?<!\\\)$delimiter/", $this->value);
+
+ // remove slashes from any semicolon and comma left escaped in the single values
+ $compoundValues = array_map(
+ function($val) {
+ return strtr($val, array('\,' => ',', '\;' => ';'));
+ }, $compoundValues);
+
+ return $compoundValues;
+
+ }
+
+ /**
+ * Returns the delimiter for this property.
+ *
+ * @return string
+ */
+ public function getDelimiter() {
+
+ if (!$this->delimiter) {
+ if (isset(self::$delimiterMap[$this->name])) {
+ $this->delimiter = self::$delimiterMap[$this->name];
+ } else {
+ // To be a bit future proof, we are going to default the
+ // delimiter to ;
+ $this->delimiter = ';';
+ }
+ }
+ return $this->delimiter;
+
+ }
+
+ /**
+ * Set a compound value as an array.
+ *
+ *
+ * @param $name string
+ * @return array
+ */
+ public function setParts(array $values) {
+
+ // add slashes to all semicolons and commas in the single values
+ $values = array_map(
+ function($val) {
+ return strtr($val, array(',' => '\,', ';' => '\;'));
+ }, $values);
+
+ $this->setValue(
+ implode($this->getDelimiter(), $values)
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php
new file mode 100644
index 0000000..36299c9
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/DateTime.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * DateTime property
+ *
+ * This element is used for iCalendar properties such as the DTSTART property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTime and getDateTime to
+ * retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class DateTime extends VObject\Property {
+
+ /**
+ * Local 'floating' time
+ */
+ const LOCAL = 1;
+
+ /**
+ * UTC-based time
+ */
+ const UTC = 2;
+
+ /**
+ * Local time plus timezone
+ */
+ const LOCALTZ = 3;
+
+ /**
+ * Only a date, time is ignored
+ */
+ const DATE = 4;
+
+ /**
+ * DateTime representation
+ *
+ * @var \DateTime
+ */
+ protected $dateTime;
+
+ /**
+ * dateType
+ *
+ * @var int
+ */
+ protected $dateType;
+
+ /**
+ * Updates the Date and Time.
+ *
+ * @param \DateTime $dt
+ * @param int $dateType
+ * @return void
+ */
+ public function setDateTime(\DateTime $dt, $dateType = self::LOCALTZ) {
+
+ switch($dateType) {
+
+ case self::LOCAL :
+ $this->setValue($dt->format('Ymd\\THis'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case self::UTC :
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ $this->setValue($dt->format('Ymd\\THis\\Z'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case self::LOCALTZ :
+ $this->setValue($dt->format('Ymd\\THis'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE-TIME');
+ $this->offsetSet('TZID', $dt->getTimeZone()->getName());
+ break;
+ case self::DATE :
+ $this->setValue($dt->format('Ymd'));
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ $this->offsetSet('VALUE','DATE');
+ break;
+ default :
+ throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+ }
+ $this->dateTime = $dt;
+ $this->dateType = $dateType;
+
+ }
+
+ /**
+ * Returns the current DateTime value.
+ *
+ * If no value was set, this method returns null.
+ *
+ * @return \DateTime|null
+ */
+ public function getDateTime() {
+
+ if ($this->dateTime)
+ return $this->dateTime;
+
+ list(
+ $this->dateType,
+ $this->dateTime
+ ) = self::parseData($this->value, $this);
+ return $this->dateTime;
+
+ }
+
+ /**
+ * Returns the type of Date format.
+ *
+ * This method returns one of the format constants. If no date was set,
+ * this method will return null.
+ *
+ * @return int|null
+ */
+ public function getDateType() {
+
+ if ($this->dateType)
+ return $this->dateType;
+
+ list(
+ $this->dateType,
+ $this->dateTime,
+ ) = self::parseData($this->value, $this);
+ return $this->dateType;
+
+ }
+
+ /**
+ * This method will return true, if the property had a date and a time, as
+ * opposed to only a date.
+ *
+ * @return bool
+ */
+ public function hasTime() {
+
+ return $this->getDateType()!==self::DATE;
+
+ }
+
+ /**
+ * Parses the internal data structure to figure out what the current date
+ * and time is.
+ *
+ * The returned array contains two elements:
+ * 1. A 'DateType' constant (as defined on this class), or null.
+ * 2. A DateTime object (or null)
+ *
+ * @param string|null $propertyValue The string to parse (yymmdd or
+ * ymmddThhmmss, etc..)
+ * @param \OldSabre\VObject\Property|null $property The instance of the
+ * property we're parsing.
+ * @return array
+ */
+ static public function parseData($propertyValue, VObject\Property $property = null) {
+
+ if (is_null($propertyValue)) {
+ return array(null, null);
+ }
+
+ $date = '(?P<year>[1-2][0-9]{3})(?P<month>[0-1][0-9])(?P<date>[0-3][0-9])';
+ $time = '(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9])';
+ $regex = "/^$date(T$time(?P<isutc>Z)?)?$/";
+
+ if (!preg_match($regex, $propertyValue, $matches)) {
+ throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string');
+ }
+
+ if (!isset($matches['hour'])) {
+ // Date-only
+ return array(
+ self::DATE,
+ new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')),
+ );
+ }
+
+ $dateStr =
+ $matches['year'] .'-' .
+ $matches['month'] . '-' .
+ $matches['date'] . ' ' .
+ $matches['hour'] . ':' .
+ $matches['minute'] . ':' .
+ $matches['second'];
+
+ if (isset($matches['isutc'])) {
+ $dt = new \DateTime($dateStr,new \DateTimeZone('UTC'));
+ $dt->setTimeZone(new \DateTimeZone('UTC'));
+ return array(
+ self::UTC,
+ $dt
+ );
+ }
+
+ // Finding the timezone.
+ $tzid = $property['TZID'];
+ if (!$tzid) {
+ // This was a floating time string. This implies we use the
+ // timezone from date_default_timezone_set / date.timezone ini
+ // setting.
+ return array(
+ self::LOCAL,
+ new \DateTime($dateStr)
+ );
+ }
+
+ // To look up the timezone, we must first find the VCALENDAR component.
+ $root = $property;
+ while($root->parent) {
+ $root = $root->parent;
+ }
+ if ($root->name === 'VCALENDAR') {
+ $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root);
+ } else {
+ $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid);
+ }
+
+ $dt = new \DateTime($dateStr, $tz);
+ $dt->setTimeZone($tz);
+
+ return array(
+ self::LOCALTZ,
+ $dt
+ );
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php
new file mode 100644
index 0000000..363bc5d
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Property/MultiDateTime.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace OldSabre\VObject\Property;
+
+use OldSabre\VObject;
+
+/**
+ * Multi-DateTime property
+ *
+ * This element is used for iCalendar properties such as the EXDATE property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTimes and getDateTimes
+ * to retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class MultiDateTime extends VObject\Property {
+
+ /**
+ * DateTime representation
+ *
+ * @var DateTime[]
+ */
+ protected $dateTimes;
+
+ /**
+ * dateType
+ *
+ * This is one of the OldSabre\VObject\Property\DateTime constants.
+ *
+ * @var int
+ */
+ protected $dateType;
+
+ /**
+ * Updates the value
+ *
+ * @param array $dt Must be an array of DateTime objects.
+ * @param int $dateType
+ * @return void
+ */
+ public function setDateTimes(array $dt, $dateType = VObject\Property\DateTime::LOCALTZ) {
+
+ foreach($dt as $i)
+ if (!$i instanceof \DateTime)
+ throw new \InvalidArgumentException('You must pass an array of DateTime objects');
+
+ $this->offsetUnset('VALUE');
+ $this->offsetUnset('TZID');
+ switch($dateType) {
+
+ case DateTime::LOCAL :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd\\THis');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case DateTime::UTC :
+ $val = array();
+ foreach($dt as $i) {
+ $i->setTimeZone(new \DateTimeZone('UTC'));
+ $val[] = $i->format('Ymd\\THis\\Z');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ break;
+ case DateTime::LOCALTZ :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd\\THis');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE-TIME');
+ $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName());
+ break;
+ case DateTime::DATE :
+ $val = array();
+ foreach($dt as $i) {
+ $val[] = $i->format('Ymd');
+ }
+ $this->setValue(implode(',',$val));
+ $this->offsetSet('VALUE','DATE');
+ break;
+ default :
+ throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+ }
+ $this->dateTimes = $dt;
+ $this->dateType = $dateType;
+
+ }
+
+ /**
+ * Returns the current DateTime value.
+ *
+ * If no value was set, this method returns null.
+ *
+ * @return array|null
+ */
+ public function getDateTimes() {
+
+ if ($this->dateTimes)
+ return $this->dateTimes;
+
+ $dts = array();
+
+ if (!$this->value) {
+ $this->dateTimes = null;
+ $this->dateType = null;
+ return null;
+ }
+
+ foreach(explode(',',$this->value) as $val) {
+ list(
+ $type,
+ $dt
+ ) = DateTime::parseData($val, $this);
+ $dts[] = $dt;
+ $this->dateType = $type;
+ }
+ $this->dateTimes = $dts;
+ return $this->dateTimes;
+
+ }
+
+ /**
+ * Returns the type of Date format.
+ *
+ * This method returns one of the format constants. If no date was set,
+ * this method will return null.
+ *
+ * @return int|null
+ */
+ public function getDateType() {
+
+ if ($this->dateType)
+ return $this->dateType;
+
+ if (!$this->value) {
+ $this->dateTimes = null;
+ $this->dateType = null;
+ return null;
+ }
+
+ $dts = array();
+ foreach(explode(',',$this->value) as $val) {
+ list(
+ $type,
+ $dt
+ ) = DateTime::parseData($val, $this);
+ $dts[] = $dt;
+ $this->dateType = $type;
+ }
+ $this->dateTimes = $dts;
+ return $this->dateType;
+
+ }
+
+ /**
+ * This method will return true, if the property had a date and a time, as
+ * opposed to only a date.
+ *
+ * @return bool
+ */
+ public function hasTime() {
+
+ return $this->getDateType()!==DateTime::DATE;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php
new file mode 100644
index 0000000..bc9bfc7
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Reader.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * VCALENDAR/VCARD reader
+ *
+ * This class reads the vobject file, and returns a full element tree.
+ *
+ * TODO: this class currently completely works 'statically'. This is pointless,
+ * and defeats OOP principals. Needs refactoring in a future version.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Reader {
+
+ /**
+ * If this option is passed to the reader, it will be less strict about the
+ * validity of the lines.
+ *
+ * Currently using this option just means, that it will accept underscores
+ * in property names.
+ */
+ const OPTION_FORGIVING = 1;
+
+ /**
+ * If this option is turned on, any lines we cannot parse will be ignored
+ * by the reader.
+ */
+ const OPTION_IGNORE_INVALID_LINES = 2;
+
+ /**
+ * Parses the file and returns the top component
+ *
+ * The options argument is a bitfield. Pass any of the OPTIONS constant to
+ * alter the parsers' behaviour.
+ *
+ * @param string $data
+ * @param int $options
+ * @return Node
+ */
+ static function read($data, $options = 0) {
+
+ // Normalizing newlines
+ $data = str_replace(array("\r","\n\n"), array("\n","\n"), $data);
+
+ $lines = explode("\n", $data);
+
+ // Unfolding lines
+ $lines2 = array();
+ foreach($lines as $line) {
+
+ // Skipping empty lines
+ if (!$line) continue;
+
+ if ($line[0]===" " || $line[0]==="\t") {
+ $lines2[count($lines2)-1].=substr($line,1);
+ } else {
+ $lines2[] = $line;
+ }
+
+ }
+
+ unset($lines);
+
+ reset($lines2);
+
+ return self::readLine($lines2, $options);
+
+ }
+
+ /**
+ * Reads and parses a single line.
+ *
+ * This method receives the full array of lines. The array pointer is used
+ * to traverse.
+ *
+ * This method returns null if an invalid line was encountered, and the
+ * IGNORE_INVALID_LINES option was turned on.
+ *
+ * @param array $lines
+ * @param int $options See the OPTIONS constants.
+ * @return Node
+ */
+ static private function readLine(&$lines, $options = 0) {
+
+ $line = current($lines);
+ $lineNr = key($lines);
+ next($lines);
+
+ // Components
+ if (strtoupper(substr($line,0,6)) === "BEGIN:") {
+
+ $componentName = strtoupper(substr($line,6));
+ $obj = Component::create($componentName);
+
+ $nextLine = current($lines);
+
+ while(strtoupper(substr($nextLine,0,4))!=="END:") {
+
+ $parsedLine = self::readLine($lines, $options);
+ $nextLine = current($lines);
+
+ if (is_null($parsedLine)) {
+ continue;
+ }
+ $obj->add($parsedLine);
+
+ if ($nextLine===false)
+ throw new ParseException('Invalid VObject. Document ended prematurely.');
+
+ }
+
+ // Checking component name of the 'END:' line.
+ if (substr($nextLine,4)!==$obj->name) {
+ throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"');
+ }
+ next($lines);
+
+ return $obj;
+
+ }
+
+ // Properties
+ //$result = preg_match('/(?P<name>[A-Z0-9-]+)(?:;(?P<parameters>^(?<!:):))(.*)$/',$line,$matches);
+
+ if ($options & self::OPTION_FORGIVING) {
+ $token = '[A-Z0-9-\._]+';
+ } else {
+ $token = '[A-Z0-9-\.]+';
+ }
+ $parameters = "(?:;(?P<parameters>([^:^\"]|\"([^\"]*)\")*))?";
+ $regex = "/^(?P<name>$token)$parameters:(?P<value>.*)$/i";
+
+ $result = preg_match($regex,$line,$matches);
+
+ if (!$result) {
+ if ($options & self::OPTION_IGNORE_INVALID_LINES) {
+ return null;
+ } else {
+ throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format');
+ }
+ }
+
+ $propertyName = strtoupper($matches['name']);
+ $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) {
+ if ($matches[2]==='n' || $matches[2]==='N') {
+ return "\n";
+ } else {
+ return $matches[2];
+ }
+ }, $matches['value']);
+
+ $obj = Property::create($propertyName, $propertyValue);
+
+ if ($matches['parameters']) {
+
+ foreach(self::readParameters($matches['parameters']) as $param) {
+ $obj->add($param);
+ }
+
+ }
+
+ return $obj;
+
+
+ }
+
+ /**
+ * Reads a parameter list from a property
+ *
+ * This method returns an array of Parameter
+ *
+ * @param string $parameters
+ * @return array
+ */
+ static private function readParameters($parameters) {
+
+ $token = '[A-Z0-9-]+';
+
+ $paramValue = '(?P<paramValue>[^\"^;]*|"[^"]*")';
+
+ $regex = "/(?<=^|;)(?P<paramName>$token)(=$paramValue(?=$|;))?/i";
+ preg_match_all($regex, $parameters, $matches, PREG_SET_ORDER);
+
+ $params = array();
+ foreach($matches as $match) {
+
+ if (!isset($match['paramValue'])) {
+
+ $value = null;
+
+ } else {
+
+ $value = $match['paramValue'];
+
+ if (isset($value[0]) && $value[0]==='"') {
+ // Stripping quotes, if needed
+ $value = substr($value,1,strlen($value)-2);
+ }
+
+ $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) {
+ if ($matches[2]==='n' || $matches[2]==='N') {
+ return "\n";
+ } else {
+ return $matches[2];
+ }
+ }, $value);
+
+ }
+
+ $params[] = new Parameter($match['paramName'], $value);
+
+ }
+
+ return $params;
+
+ }
+
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php
new file mode 100644
index 0000000..64bd994
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/RecurrenceIterator.php
@@ -0,0 +1,1112 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class is used to determine new for a recurring event, when the next
+ * events occur.
+ *
+ * This iterator may loop infinitely in the future, therefore it is important
+ * that if you use this class, you set hard limits for the amount of iterations
+ * you want to handle.
+ *
+ * Note that currently there is not full support for the entire iCalendar
+ * specification, as it's very complex and contains a lot of permutations
+ * that's not yet used very often in software.
+ *
+ * For the focus has been on features as they actually appear in Calendaring
+ * software, but this may well get expanded as needed / on demand
+ *
+ * The following RRULE properties are supported
+ * * UNTIL
+ * * INTERVAL
+ * * COUNT
+ * * FREQ=DAILY
+ * * BYDAY
+ * * BYHOUR
+ * * FREQ=WEEKLY
+ * * BYDAY
+ * * BYHOUR
+ * * WKST
+ * * FREQ=MONTHLY
+ * * BYMONTHDAY
+ * * BYDAY
+ * * BYSETPOS
+ * * FREQ=YEARLY
+ * * BYMONTH
+ * * BYMONTHDAY (only if BYMONTH is also set)
+ * * BYDAY (only if BYMONTH is also set)
+ *
+ * Anything beyond this is 'undefined', which means that it may get ignored, or
+ * you may get unexpected results. The effect is that in some applications the
+ * specified recurrence may look incorrect, or is missing.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class RecurrenceIterator implements \Iterator {
+
+ /**
+ * The initial event date
+ *
+ * @var DateTime
+ */
+ public $startDate;
+
+ /**
+ * The end-date of the initial event
+ *
+ * @var DateTime
+ */
+ public $endDate;
+
+ /**
+ * The 'current' recurrence.
+ *
+ * This will be increased for every iteration.
+ *
+ * @var DateTime
+ */
+ public $currentDate;
+
+
+ /**
+ * List of dates that are excluded from the rules.
+ *
+ * This list contains the items that have been overriden by the EXDATE
+ * property.
+ *
+ * @var array
+ */
+ public $exceptionDates = array();
+
+ /**
+ * Base event
+ *
+ * @var Component\VEvent
+ */
+ public $baseEvent;
+
+ /**
+ * List of dates that are overridden by other events.
+ * Similar to $overriddenEvents, but this just contains the original dates.
+ *
+ * @var array
+ */
+ public $overriddenDates = array();
+
+ /**
+ * list of events that are 'overridden'.
+ *
+ * This is an array of Component\VEvent objects.
+ *
+ * @var array
+ */
+ public $overriddenEvents = array();
+
+
+ /**
+ * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
+ * yearly.
+ *
+ * @var string
+ */
+ public $frequency;
+
+ /**
+ * The last instance of this recurrence, inclusively
+ *
+ * @var DateTime|null
+ */
+ public $until;
+
+ /**
+ * The number of recurrences, or 'null' if infinitely recurring.
+ *
+ * @var int
+ */
+ public $count;
+
+ /**
+ * The interval.
+ *
+ * If for example frequency is set to daily, interval = 2 would mean every
+ * 2 days.
+ *
+ * @var int
+ */
+ public $interval = 1;
+
+ /**
+ * Which seconds to recur.
+ *
+ * This is an array of integers (between 0 and 60)
+ *
+ * @var array
+ */
+ public $bySecond;
+
+ /**
+ * Which minutes to recur
+ *
+ * This is an array of integers (between 0 and 59)
+ *
+ * @var array
+ */
+ public $byMinute;
+
+ /**
+ * Which hours to recur
+ *
+ * This is an array of integers (between 0 and 23)
+ *
+ * @var array
+ */
+ public $byHour;
+
+ /**
+ * Which weekdays to recur.
+ *
+ * This is an array of weekdays
+ *
+ * This may also be preceeded by a positive or negative integer. If present,
+ * this indicates the nth occurrence of a specific day within the monthly or
+ * yearly rrule. For instance, -2TU indicates the second-last tuesday of
+ * the month, or year.
+ *
+ * @var array
+ */
+ public $byDay;
+
+ /**
+ * Which days of the month to recur
+ *
+ * This is an array of days of the months (1-31). The value can also be
+ * negative. -5 for instance means the 5th last day of the month.
+ *
+ * @var array
+ */
+ public $byMonthDay;
+
+ /**
+ * Which days of the year to recur.
+ *
+ * This is an array with days of the year (1 to 366). The values can also
+ * be negative. For instance, -1 will always represent the last day of the
+ * year. (December 31st).
+ *
+ * @var array
+ */
+ public $byYearDay;
+
+ /**
+ * Which week numbers to recur.
+ *
+ * This is an array of integers from 1 to 53. The values can also be
+ * negative. -1 will always refer to the last week of the year.
+ *
+ * @var array
+ */
+ public $byWeekNo;
+
+ /**
+ * Which months to recur
+ *
+ * This is an array of integers from 1 to 12.
+ *
+ * @var array
+ */
+ public $byMonth;
+
+ /**
+ * Which items in an existing st to recur.
+ *
+ * These numbers work together with an existing by* rule. It specifies
+ * exactly which items of the existing by-rule to filter.
+ *
+ * Valid values are 1 to 366 and -1 to -366. As an example, this can be
+ * used to recur the last workday of the month.
+ *
+ * This would be done by setting frequency to 'monthly', byDay to
+ * 'MO,TU,WE,TH,FR' and bySetPos to -1.
+ *
+ * @var array
+ */
+ public $bySetPos;
+
+ /**
+ * When a week starts
+ *
+ * @var string
+ */
+ public $weekStart = 'MO';
+
+ /**
+ * The current item in the list
+ *
+ * @var int
+ */
+ public $counter = 0;
+
+ /**
+ * Simple mapping from iCalendar day names to day numbers
+ *
+ * @var array
+ */
+ private $dayMap = array(
+ 'SU' => 0,
+ 'MO' => 1,
+ 'TU' => 2,
+ 'WE' => 3,
+ 'TH' => 4,
+ 'FR' => 5,
+ 'SA' => 6,
+ );
+
+ /**
+ * Mappings between the day number and english day name.
+ *
+ * @var array
+ */
+ private $dayNames = array(
+ 0 => 'Sunday',
+ 1 => 'Monday',
+ 2 => 'Tuesday',
+ 3 => 'Wednesday',
+ 4 => 'Thursday',
+ 5 => 'Friday',
+ 6 => 'Saturday',
+ );
+
+ /**
+ * If the current iteration of the event is an overriden event, this
+ * property will hold the VObject
+ *
+ * @var Component
+ */
+ private $currentOverriddenEvent;
+
+ /**
+ * This property may contain the date of the next not-overridden event.
+ * This date is calculated sometimes a bit early, before overridden events
+ * are evaluated.
+ *
+ * @var DateTime
+ */
+ private $nextDate;
+
+ /**
+ * Creates the iterator
+ *
+ * You should pass a VCALENDAR component, as well as the UID of the event
+ * we're going to traverse.
+ *
+ * @param Component $vcal
+ * @param string|null $uid
+ */
+ public function __construct(Component $vcal, $uid=null) {
+
+ if (is_null($uid)) {
+ if ($vcal->name === 'VCALENDAR') {
+ throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
+ }
+ $components = array($vcal);
+ $uid = (string)$vcal->uid;
+ } else {
+ $components = $vcal->select('VEVENT');
+ }
+ foreach($components as $component) {
+ if ((string)$component->uid == $uid) {
+ if (isset($component->{'RECURRENCE-ID'})) {
+ $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component;
+ $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime();
+ } else {
+ $this->baseEvent = $component;
+ }
+ }
+ }
+ if (!$this->baseEvent) {
+ throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid);
+ }
+
+ $this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
+
+ $this->endDate = null;
+ if (isset($this->baseEvent->DTEND)) {
+ $this->endDate = clone $this->baseEvent->DTEND->getDateTime();
+ } else {
+ $this->endDate = clone $this->startDate;
+ if (isset($this->baseEvent->DURATION)) {
+ $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value));
+ } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) {
+ $this->endDate->modify('+1 day');
+ }
+ }
+ $this->currentDate = clone $this->startDate;
+
+ $rrule = (string)$this->baseEvent->RRULE;
+
+ $parts = explode(';', $rrule);
+
+ // If no rrule was specified, we create a default setting
+ if (!$rrule) {
+ $this->frequency = 'daily';
+ $this->count = 1;
+ } else foreach($parts as $part) {
+
+ list($key, $value) = explode('=', $part, 2);
+
+ switch(strtoupper($key)) {
+
+ case 'FREQ' :
+ if (!in_array(
+ strtolower($value),
+ array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
+ )) {
+ throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
+
+ }
+ $this->frequency = strtolower($value);
+ break;
+
+ case 'UNTIL' :
+ $this->until = DateTimeParser::parse($value);
+
+ // In some cases events are generated with an UNTIL=
+ // parameter before the actual start of the event.
+ //
+ // Not sure why this is happening. We assume that the
+ // intention was that the event only recurs once.
+ //
+ // So we are modifying the parameter so our code doesn't
+ // break.
+ if($this->until < $this->baseEvent->DTSTART->getDateTime()) {
+ $this->until = $this->baseEvent->DTSTART->getDateTime();
+ }
+ break;
+
+ case 'COUNT' :
+ $this->count = (int)$value;
+ break;
+
+ case 'INTERVAL' :
+ $this->interval = (int)$value;
+ if ($this->interval < 1) {
+ throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!');
+ }
+ break;
+
+ case 'BYSECOND' :
+ $this->bySecond = explode(',', $value);
+ break;
+
+ case 'BYMINUTE' :
+ $this->byMinute = explode(',', $value);
+ break;
+
+ case 'BYHOUR' :
+ $this->byHour = explode(',', $value);
+ break;
+
+ case 'BYDAY' :
+ $this->byDay = explode(',', strtoupper($value));
+ break;
+
+ case 'BYMONTHDAY' :
+ $this->byMonthDay = explode(',', $value);
+ break;
+
+ case 'BYYEARDAY' :
+ $this->byYearDay = explode(',', $value);
+ break;
+
+ case 'BYWEEKNO' :
+ $this->byWeekNo = explode(',', $value);
+ break;
+
+ case 'BYMONTH' :
+ $this->byMonth = explode(',', $value);
+ break;
+
+ case 'BYSETPOS' :
+ $this->bySetPos = explode(',', $value);
+ break;
+
+ case 'WKST' :
+ $this->weekStart = strtoupper($value);
+ break;
+
+ }
+
+ }
+
+ // Parsing exception dates
+ if (isset($this->baseEvent->EXDATE)) {
+ foreach($this->baseEvent->EXDATE as $exDate) {
+
+ foreach(explode(',', (string)$exDate) as $exceptionDate) {
+
+ $this->exceptionDates[] =
+ DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
+
+ }
+
+ }
+
+ }
+
+ }
+
+ /**
+ * Returns the current item in the list
+ *
+ * @return DateTime
+ */
+ public function current() {
+
+ if (!$this->valid()) return null;
+ return clone $this->currentDate;
+
+ }
+
+ /**
+ * This method returns the startdate for the current iteration of the
+ * event.
+ *
+ * @return DateTime
+ */
+ public function getDtStart() {
+
+ if (!$this->valid()) return null;
+ return clone $this->currentDate;
+
+ }
+
+ /**
+ * This method returns the enddate for the current iteration of the
+ * event.
+ *
+ * @return DateTime
+ */
+ public function getDtEnd() {
+
+ if (!$this->valid()) return null;
+ $dtEnd = clone $this->currentDate;
+ $dtEnd->add( $this->startDate->diff( $this->endDate ) );
+ return clone $dtEnd;
+
+ }
+
+ /**
+ * Returns a VEVENT object with the updated start and end date.
+ *
+ * Any recurrence information is removed, and this function may return an
+ * 'overridden' event instead.
+ *
+ * This method always returns a cloned instance.
+ *
+ * @return Component\VEvent
+ */
+ public function getEventObject() {
+
+ if ($this->currentOverriddenEvent) {
+ return clone $this->currentOverriddenEvent;
+ }
+ $event = clone $this->baseEvent;
+ unset($event->RRULE);
+ unset($event->EXDATE);
+ unset($event->RDATE);
+ unset($event->EXRULE);
+
+ $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
+ if (isset($event->DTEND)) {
+ $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
+ }
+ if ($this->counter > 0) {
+ $event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
+ }
+
+ return $event;
+
+ }
+
+ /**
+ * Returns the current item number
+ *
+ * @return int
+ */
+ public function key() {
+
+ return $this->counter;
+
+ }
+
+ /**
+ * Whether or not there is a 'next item'
+ *
+ * @return bool
+ */
+ public function valid() {
+
+ if (!is_null($this->count)) {
+ return $this->counter < $this->count;
+ }
+ if (!is_null($this->until)) {
+ return $this->currentDate <= $this->until;
+ }
+ return true;
+
+ }
+
+ /**
+ * Resets the iterator
+ *
+ * @return void
+ */
+ public function rewind() {
+
+ $this->currentDate = clone $this->startDate;
+ $this->counter = 0;
+
+ }
+
+ /**
+ * This method allows you to quickly go to the next occurrence after the
+ * specified date.
+ *
+ * Note that this checks the current 'endDate', not the 'stardDate'. This
+ * means that if you forward to January 1st, the iterator will stop at the
+ * first event that ends *after* January 1st.
+ *
+ * @param DateTime $dt
+ * @return void
+ */
+ public function fastForward(\DateTime $dt) {
+
+ while($this->valid() && $this->getDTEnd() <= $dt) {
+ $this->next();
+ }
+
+ }
+
+ /**
+ * Returns true if this recurring event never ends.
+ *
+ * @return bool
+ */
+ public function isInfinite() {
+
+ return !$this->count && !$this->until;
+
+ }
+
+ /**
+ * Goes on to the next iteration
+ *
+ * @return void
+ */
+ public function next() {
+
+ /*
+ if (!is_null($this->count) && $this->counter >= $this->count) {
+ $this->currentDate = null;
+ }*/
+
+
+ $previousStamp = $this->currentDate->getTimeStamp();
+
+ while(true) {
+
+ $this->currentOverriddenEvent = null;
+
+ // If we have a next date 'stored', we use that
+ if ($this->nextDate) {
+ $this->currentDate = $this->nextDate;
+ $currentStamp = $this->currentDate->getTimeStamp();
+ $this->nextDate = null;
+ } else {
+
+ // Otherwise, we calculate it
+ switch($this->frequency) {
+
+ case 'hourly' :
+ $this->nextHourly();
+ break;
+
+ case 'daily' :
+ $this->nextDaily();
+ break;
+
+ case 'weekly' :
+ $this->nextWeekly();
+ break;
+
+ case 'monthly' :
+ $this->nextMonthly();
+ break;
+
+ case 'yearly' :
+ $this->nextYearly();
+ break;
+
+ }
+ $currentStamp = $this->currentDate->getTimeStamp();
+
+ // Checking exception dates
+ foreach($this->exceptionDates as $exceptionDate) {
+ if ($this->currentDate == $exceptionDate) {
+ $this->counter++;
+ continue 2;
+ }
+ }
+ foreach($this->overriddenDates as $overriddenDate) {
+ if ($this->currentDate == $overriddenDate) {
+ continue 2;
+ }
+ }
+
+ }
+
+ // Checking overridden events
+ foreach($this->overriddenEvents as $index=>$event) {
+ if ($index > $previousStamp && $index <= $currentStamp) {
+
+ // We're moving the 'next date' aside, for later use.
+ $this->nextDate = clone $this->currentDate;
+
+ $this->currentDate = $event->DTSTART->getDateTime();
+ $this->currentOverriddenEvent = $event;
+
+ break;
+ }
+ }
+
+ break;
+
+ }
+
+ /*
+ if (!is_null($this->until)) {
+ if($this->currentDate > $this->until) {
+ $this->currentDate = null;
+ }
+ }*/
+
+ $this->counter++;
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for hourly frequency.
+ *
+ * @return void
+ */
+ protected function nextHourly() {
+
+ if (!$this->byHour) {
+ $this->currentDate->modify('+' . $this->interval . ' hours');
+ return;
+ }
+ }
+
+ /**
+ * Does the processing for advancing the iterator for daily frequency.
+ *
+ * @return void
+ */
+ protected function nextDaily() {
+
+ if (!$this->byHour && !$this->byDay) {
+ $this->currentDate->modify('+' . $this->interval . ' days');
+ return;
+ }
+
+ if (isset($this->byHour)) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ if (isset($this->byDay)) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ do {
+
+ if ($this->byHour) {
+ if ($this->currentDate->format('G') == '23') {
+ // to obey the interval rule
+ $this->currentDate->modify('+' . $this->interval-1 . ' days');
+ }
+
+ $this->currentDate->modify('+1 hours');
+
+ } else {
+ $this->currentDate->modify('+' . $this->interval . ' days');
+
+ }
+
+ // Current day of the week
+ $currentDay = $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = $this->currentDate->format('G');
+
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for weekly frequency.
+ *
+ * @return void
+ */
+ protected function nextWeekly() {
+
+ if (!$this->byHour && !$this->byDay) {
+ $this->currentDate->modify('+' . $this->interval . ' weeks');
+ return;
+ }
+
+ if ($this->byHour) {
+ $recurrenceHours = $this->getHours();
+ }
+
+ if ($this->byDay) {
+ $recurrenceDays = $this->getDays();
+ }
+
+ // First day of the week:
+ $firstDay = $this->dayMap[$this->weekStart];
+
+ do {
+
+ if ($this->byHour) {
+ $this->currentDate->modify('+1 hours');
+ } else {
+ $this->currentDate->modify('+1 days');
+ }
+
+ // Current day of the week
+ $currentDay = (int) $this->currentDate->format('w');
+
+ // Current hour of the day
+ $currentHour = (int) $this->currentDate->format('G');
+
+ // We need to roll over to the next week
+ if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
+ $this->currentDate->modify('+' . $this->interval-1 . ' weeks');
+
+ // We need to go to the first day of this week, but only if we
+ // are not already on this first day of this week.
+ if($this->currentDate->format('w') != $firstDay) {
+ $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
+ }
+ }
+
+ // We have a match
+ } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+ }
+
+ /**
+ * Does the processing for advancing the iterator for monthly frequency.
+ *
+ * @return void
+ */
+ protected function nextMonthly() {
+
+ $currentDayOfMonth = $this->currentDate->format('j');
+ if (!$this->byMonthDay && !$this->byDay) {
+
+ // If the current day is higher than the 28th, rollover can
+ // occur to the next month. We Must skip these invalid
+ // entries.
+ if ($currentDayOfMonth < 29) {
+ $this->currentDate->modify('+' . $this->interval . ' months');
+ } else {
+ $increase = 0;
+ do {
+ $increase++;
+ $tempDate = clone $this->currentDate;
+ $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
+ } while ($tempDate->format('j') != $currentDayOfMonth);
+ $this->currentDate = $tempDate;
+ }
+ return;
+ }
+
+ while(true) {
+
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach($occurrences as $occurrence) {
+
+ // The first occurrence thats higher than the current
+ // day of the month wins.
+ if ($occurrence > $currentDayOfMonth) {
+ break 2;
+ }
+
+ }
+
+ // If we made it all the way here, it means there were no
+ // valid occurrences, and we need to advance to the next
+ // month.
+ $this->currentDate->modify('first day of this month');
+ $this->currentDate->modify('+ ' . $this->interval . ' months');
+
+ // This goes to 0 because we need to start counting at hte
+ // beginning.
+ $currentDayOfMonth = 0;
+
+ }
+
+ $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
+
+ }
+
+ /**
+ * Does the processing for advancing the iterator for yearly frequency.
+ *
+ * @return void
+ */
+ protected function nextYearly() {
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ // No sub-rules, so we just advance by year
+ if (!$this->byMonth) {
+
+ // Unless it was a leap day!
+ if ($currentMonth==2 && $currentDayOfMonth==29) {
+
+ $counter = 0;
+ do {
+ $counter++;
+ // Here we increase the year count by the interval, until
+ // we hit a date that's also in a leap year.
+ //
+ // We could just find the next interval that's dividable by
+ // 4, but that would ignore the rule that there's no leap
+ // year every year that's dividable by a 100, but not by
+ // 400. (1800, 1900, 2100). So we just rely on the datetime
+ // functions instead.
+ $nextDate = clone $this->currentDate;
+ $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
+ } while ($nextDate->format('n')!=2);
+ $this->currentDate = $nextDate;
+
+ return;
+
+ }
+
+ // The easiest form
+ $this->currentDate->modify('+' . $this->interval . ' years');
+ return;
+
+ }
+
+ $currentMonth = $this->currentDate->format('n');
+ $currentYear = $this->currentDate->format('Y');
+ $currentDayOfMonth = $this->currentDate->format('j');
+
+ $advancedToNewMonth = false;
+
+ // If we got a byDay or getMonthDay filter, we must first expand
+ // further.
+ if ($this->byDay || $this->byMonthDay) {
+
+ while(true) {
+
+ $occurrences = $this->getMonthlyOccurrences();
+
+ foreach($occurrences as $occurrence) {
+
+ // The first occurrence that's higher than the current
+ // day of the month wins.
+ // If we advanced to the next month or year, the first
+ // occurrence is always correct.
+ if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
+ break 2;
+ }
+
+ }
+
+ // If we made it here, it means we need to advance to
+ // the next month or year.
+ $currentDayOfMonth = 1;
+ $advancedToNewMonth = true;
+ do {
+
+ $currentMonth++;
+ if ($currentMonth>12) {
+ $currentYear+=$this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+
+ $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+ }
+
+ // If we made it here, it means we got a valid occurrence
+ $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
+ return;
+
+ } else {
+
+ // These are the 'byMonth' rules, if there are no byDay or
+ // byMonthDay sub-rules.
+ do {
+
+ $currentMonth++;
+ if ($currentMonth>12) {
+ $currentYear+=$this->interval;
+ $currentMonth = 1;
+ }
+ } while (!in_array($currentMonth, $this->byMonth));
+ $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+ return;
+
+ }
+
+ }
+
+ /**
+ * Returns all the occurrences for a monthly frequency with a 'byDay' or
+ * 'byMonthDay' expansion for the current month.
+ *
+ * The returned list is an array of integers with the day of month (1-31).
+ *
+ * @return array
+ */
+ protected function getMonthlyOccurrences() {
+
+ $startDate = clone $this->currentDate;
+
+ $byDayResults = array();
+
+ // Our strategy is to simply go through the byDays, advance the date to
+ // that point and add it to the results.
+ if ($this->byDay) foreach($this->byDay as $day) {
+
+ $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
+
+ // Dayname will be something like 'wednesday'. Now we need to find
+ // all wednesdays in this month.
+ $dayHits = array();
+
+ $checkDate = clone $startDate;
+ $checkDate->modify('first day of this month');
+ $checkDate->modify($dayName);
+
+ do {
+ $dayHits[] = $checkDate->format('j');
+ $checkDate->modify('next ' . $dayName);
+ } while ($checkDate->format('n') === $startDate->format('n'));
+
+ // So now we have 'all wednesdays' for month. It is however
+ // possible that the user only really wanted the 1st, 2nd or last
+ // wednesday.
+ if (strlen($day)>2) {
+ $offset = (int)substr($day,0,-2);
+
+ if ($offset>0) {
+ // It is possible that the day does not exist, such as a
+ // 5th or 6th wednesday of the month.
+ if (isset($dayHits[$offset-1])) {
+ $byDayResults[] = $dayHits[$offset-1];
+ }
+ } else {
+
+ // if it was negative we count from the end of the array
+ $byDayResults[] = $dayHits[count($dayHits) + $offset];
+ }
+ } else {
+ // There was no counter (first, second, last wednesdays), so we
+ // just need to add the all to the list).
+ $byDayResults = array_merge($byDayResults, $dayHits);
+
+ }
+
+ }
+
+ $byMonthDayResults = array();
+ if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
+
+ // Removing values that are out of range for this month
+ if ($monthDay > $startDate->format('t') ||
+ $monthDay < 0-$startDate->format('t')) {
+ continue;
+ }
+ if ($monthDay>0) {
+ $byMonthDayResults[] = $monthDay;
+ } else {
+ // Negative values
+ $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
+ }
+ }
+
+ // If there was just byDay or just byMonthDay, they just specify our
+ // (almost) final list. If both were provided, then byDay limits the
+ // list.
+ if ($this->byMonthDay && $this->byDay) {
+ $result = array_intersect($byMonthDayResults, $byDayResults);
+ } elseif ($this->byMonthDay) {
+ $result = $byMonthDayResults;
+ } else {
+ $result = $byDayResults;
+ }
+ $result = array_unique($result);
+ sort($result, SORT_NUMERIC);
+
+ // The last thing that needs checking is the BYSETPOS. If it's set, it
+ // means only certain items in the set survive the filter.
+ if (!$this->bySetPos) {
+ return $result;
+ }
+
+ $filteredResult = array();
+ foreach($this->bySetPos as $setPos) {
+
+ if ($setPos<0) {
+ $setPos = count($result)-($setPos+1);
+ }
+ if (isset($result[$setPos-1])) {
+ $filteredResult[] = $result[$setPos-1];
+ }
+ }
+
+ sort($filteredResult, SORT_NUMERIC);
+ return $filteredResult;
+
+ }
+
+ protected function getHours()
+ {
+ $recurrenceHours = array();
+ foreach($this->byHour as $byHour) {
+ $recurrenceHours[] = $byHour;
+ }
+
+ return $recurrenceHours;
+ }
+
+ protected function getDays()
+ {
+ $recurrenceDays = array();
+ foreach($this->byDay as $byDay) {
+
+ // The day may be preceeded with a positive (+n) or
+ // negative (-n) integer. However, this does not make
+ // sense in 'weekly' so we ignore it here.
+ $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
+
+ }
+
+ return $recurrenceDays;
+ }
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php
new file mode 100644
index 0000000..20b8f42
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/ICalendar.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+use OldSabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up iCalendar objects.
+ *
+ * This class expects a single VCALENDAR object with one or more
+ * calendar-objects inside. Objects with identical UID's will be combined into
+ * a single object.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ICalendar implements SplitterInterface {
+
+ /**
+ * Timezones
+ *
+ * @var array
+ */
+ protected $vtimezones = array();
+
+ /**
+ * iCalendar objects
+ *
+ * @var array
+ */
+ protected $objects = array();
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ public function __construct($input) {
+
+ $data = VObject\Reader::read(stream_get_contents($input));
+ $vtimezones = array();
+ $components = array();
+
+ foreach($data->children as $component) {
+ if (!$component instanceof VObject\Component) {
+ continue;
+ }
+
+ // Get all timezones
+ if ($component->name === 'VTIMEZONE') {
+ $this->vtimezones[(string)$component->TZID] = $component;
+ continue;
+ }
+
+ // Get component UID for recurring Events search
+ if($component->UID) {
+ $uid = (string)$component->UID;
+ } else {
+ // Generating a random UID
+ $uid = sha1(microtime()) . '-vobjectimport';
+ }
+
+ // Take care of recurring events
+ if (!array_key_exists($uid, $this->objects)) {
+ $this->objects[$uid] = VObject\Component::create('VCALENDAR');
+ }
+
+ $this->objects[$uid]->add(clone $component);
+ }
+
+ }
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ public function getNext() {
+
+ if($object=array_shift($this->objects)) {
+
+ // create our baseobject
+ $object->version = '2.0';
+ $object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+ $object->calscale = 'GREGORIAN';
+
+ // add vtimezone information to obj (if we have it)
+ foreach ($this->vtimezones as $vtimezone) {
+ $object->add($vtimezone);
+ }
+
+ return $object;
+
+ } else {
+
+ return null;
+
+ }
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php
new file mode 100644
index 0000000..3696031
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/SplitterInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+/**
+ * VObject splitter
+ *
+ * The splitter is responsible for reading a large vCard or iCalendar object,
+ * and splitting it into multiple objects.
+ *
+ * This is for example for Card and CalDAV, which require every event and vcard
+ * to exist in their own objects, instead of one large one.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+interface SplitterInterface {
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ function __construct($input);
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ function getNext();
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php
new file mode 100644
index 0000000..fc5e39b
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Splitter/VCard.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace OldSabre\VObject\Splitter;
+
+use OldSabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up VCard objects.
+ *
+ * It is assumed that the input stream contains 1 or more VCARD objects. This
+ * class checks for BEGIN:VCARD and END:VCARD and parses each encountered
+ * component individually.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCard implements SplitterInterface {
+
+ /**
+ * File handle
+ *
+ * @var resource
+ */
+ protected $input;
+
+ /**
+ * Constructor
+ *
+ * The splitter should receive an readable file stream as it's input.
+ *
+ * @param resource $input
+ */
+ public function __construct($input) {
+
+ $this->input = $input;
+
+ }
+
+ /**
+ * Every time getNext() is called, a new object will be parsed, until we
+ * hit the end of the stream.
+ *
+ * When the end is reached, null will be returned.
+ *
+ * @return OldSabre\VObject\Component|null
+ */
+ public function getNext() {
+
+ $vcard = '';
+
+ do {
+
+ if (feof($this->input)) {
+ return false;
+ }
+
+ $line = fgets($this->input);
+ $vcard .= $line;
+
+ } while(strtoupper(substr($line,0,4))!=="END:");
+
+ $object = VObject\Reader::read($vcard);
+
+ if($object->name !== 'VCARD') {
+ throw new \InvalidArgumentException("Thats no vCard!", 1);
+ }
+
+ return $object;
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php
new file mode 100644
index 0000000..1218792
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/StringUtil.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Useful utilities for working with various strings.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class StringUtil {
+
+ /**
+ * Returns true or false depending on if a string is valid UTF-8
+ *
+ * @param string $str
+ * @return bool
+ */
+ static function isUTF8($str) {
+
+ // First check.. mb_check_encoding
+ if (!mb_check_encoding($str, 'UTF-8')) {
+ return false;
+ }
+
+ // Control characters
+ if (preg_match('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', $str)) {
+ return false;
+ }
+
+ return true;
+
+ }
+
+ /**
+ * This method tries its best to convert the input string to UTF-8.
+ *
+ * Currently only ISO-5991-1 input and UTF-8 input is supported, but this
+ * may be expanded upon if we receive other examples.
+ *
+ * @param string $str
+ * @return string
+ */
+ static function convertToUTF8($str) {
+
+ $encoding = mb_detect_encoding($str , array('UTF-8','ISO-8859-1'), true);
+
+ if ($encoding === 'ISO-8859-1') {
+ $newStr = utf8_encode($str);
+ } else {
+ $newStr = $str;
+ }
+
+ // Removing any control characters
+ return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', '', $newStr));
+
+ }
+
+}
+
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php
new file mode 100644
index 0000000..dbb77b0
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/TimeZoneUtil.php
@@ -0,0 +1,482 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * Time zone name translation
+ *
+ * This file translates well-known time zone names into "Olson database" time zone names.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Frank Edelhaeuser (fedel@users.sourceforge.net)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class TimeZoneUtil {
+
+ public static $map = array(
+
+ // from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
+ // snapshot taken on 2012/01/16
+
+ // windows
+ 'AUS Central Standard Time'=>'Australia/Darwin',
+ 'AUS Eastern Standard Time'=>'Australia/Sydney',
+ 'Afghanistan Standard Time'=>'Asia/Kabul',
+ 'Alaskan Standard Time'=>'America/Anchorage',
+ 'Arab Standard Time'=>'Asia/Riyadh',
+ 'Arabian Standard Time'=>'Asia/Dubai',
+ 'Arabic Standard Time'=>'Asia/Baghdad',
+ 'Argentina Standard Time'=>'America/Buenos_Aires',
+ 'Armenian Standard Time'=>'Asia/Yerevan',
+ 'Atlantic Standard Time'=>'America/Halifax',
+ 'Azerbaijan Standard Time'=>'Asia/Baku',
+ 'Azores Standard Time'=>'Atlantic/Azores',
+ 'Bangladesh Standard Time'=>'Asia/Dhaka',
+ 'Canada Central Standard Time'=>'America/Regina',
+ 'Cape Verde Standard Time'=>'Atlantic/Cape_Verde',
+ 'Caucasus Standard Time'=>'Asia/Yerevan',
+ 'Cen. Australia Standard Time'=>'Australia/Adelaide',
+ 'Central America Standard Time'=>'America/Guatemala',
+ 'Central Asia Standard Time'=>'Asia/Almaty',
+ 'Central Brazilian Standard Time'=>'America/Cuiaba',
+ 'Central Europe Standard Time'=>'Europe/Budapest',
+ 'Central European Standard Time'=>'Europe/Warsaw',
+ 'Central Pacific Standard Time'=>'Pacific/Guadalcanal',
+ 'Central Standard Time'=>'America/Chicago',
+ 'Central Standard Time (Mexico)'=>'America/Mexico_City',
+ 'China Standard Time'=>'Asia/Shanghai',
+ 'Dateline Standard Time'=>'Etc/GMT+12',
+ 'E. Africa Standard Time'=>'Africa/Nairobi',
+ 'E. Australia Standard Time'=>'Australia/Brisbane',
+ 'E. Europe Standard Time'=>'Europe/Minsk',
+ 'E. South America Standard Time'=>'America/Sao_Paulo',
+ 'Eastern Standard Time'=>'America/New_York',
+ 'Egypt Standard Time'=>'Africa/Cairo',
+ 'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg',
+ 'FLE Standard Time'=>'Europe/Kiev',
+ 'Fiji Standard Time'=>'Pacific/Fiji',
+ 'GMT Standard Time'=>'Europe/London',
+ 'GTB Standard Time'=>'Europe/Istanbul',
+ 'Georgian Standard Time'=>'Asia/Tbilisi',
+ 'Greenland Standard Time'=>'America/Godthab',
+ 'Greenwich Standard Time'=>'Atlantic/Reykjavik',
+ 'Hawaiian Standard Time'=>'Pacific/Honolulu',
+ 'India Standard Time'=>'Asia/Calcutta',
+ 'Iran Standard Time'=>'Asia/Tehran',
+ 'Israel Standard Time'=>'Asia/Jerusalem',
+ 'Jordan Standard Time'=>'Asia/Amman',
+ 'Kamchatka Standard Time'=>'Asia/Kamchatka',
+ 'Korea Standard Time'=>'Asia/Seoul',
+ 'Magadan Standard Time'=>'Asia/Magadan',
+ 'Mauritius Standard Time'=>'Indian/Mauritius',
+ 'Mexico Standard Time'=>'America/Mexico_City',
+ 'Mexico Standard Time 2'=>'America/Chihuahua',
+ 'Mid-Atlantic Standard Time'=>'Etc/GMT-2',
+ 'Middle East Standard Time'=>'Asia/Beirut',
+ 'Montevideo Standard Time'=>'America/Montevideo',
+ 'Morocco Standard Time'=>'Africa/Casablanca',
+ 'Mountain Standard Time'=>'America/Denver',
+ 'Mountain Standard Time (Mexico)'=>'America/Chihuahua',
+ 'Myanmar Standard Time'=>'Asia/Rangoon',
+ 'N. Central Asia Standard Time'=>'Asia/Novosibirsk',
+ 'Namibia Standard Time'=>'Africa/Windhoek',
+ 'Nepal Standard Time'=>'Asia/Katmandu',
+ 'New Zealand Standard Time'=>'Pacific/Auckland',
+ 'Newfoundland Standard Time'=>'America/St_Johns',
+ 'North Asia East Standard Time'=>'Asia/Irkutsk',
+ 'North Asia Standard Time'=>'Asia/Krasnoyarsk',
+ 'Pacific SA Standard Time'=>'America/Santiago',
+ 'Pacific Standard Time'=>'America/Los_Angeles',
+ 'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel',
+ 'Pakistan Standard Time'=>'Asia/Karachi',
+ 'Paraguay Standard Time'=>'America/Asuncion',
+ 'Romance Standard Time'=>'Europe/Paris',
+ 'Russian Standard Time'=>'Europe/Moscow',
+ 'SA Eastern Standard Time'=>'America/Cayenne',
+ 'SA Pacific Standard Time'=>'America/Bogota',
+ 'SA Western Standard Time'=>'America/La_Paz',
+ 'SE Asia Standard Time'=>'Asia/Bangkok',
+ 'Samoa Standard Time'=>'Pacific/Apia',
+ 'Singapore Standard Time'=>'Asia/Singapore',
+ 'South Africa Standard Time'=>'Africa/Johannesburg',
+ 'Sri Lanka Standard Time'=>'Asia/Colombo',
+ 'Syria Standard Time'=>'Asia/Damascus',
+ 'Taipei Standard Time'=>'Asia/Taipei',
+ 'Tasmania Standard Time'=>'Australia/Hobart',
+ 'Tokyo Standard Time'=>'Asia/Tokyo',
+ 'Tonga Standard Time'=>'Pacific/Tongatapu',
+ 'US Eastern Standard Time'=>'America/Indianapolis',
+ 'US Mountain Standard Time'=>'America/Phoenix',
+ 'UTC+12'=>'Etc/GMT-12',
+ 'UTC-02'=>'Etc/GMT+2',
+ 'UTC-11'=>'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time'=>'America/Caracas',
+ 'Vladivostok Standard Time'=>'Asia/Vladivostok',
+ 'W. Australia Standard Time'=>'Australia/Perth',
+ 'W. Central Africa Standard Time'=>'Africa/Lagos',
+ 'W. Europe Standard Time'=>'Europe/Berlin',
+ 'West Asia Standard Time'=>'Asia/Tashkent',
+ 'West Pacific Standard Time'=>'Pacific/Port_Moresby',
+ 'Yakutsk Standard Time'=>'Asia/Yakutsk',
+
+ // Microsoft exchange timezones
+ // Source:
+ // http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx
+ //
+ // Correct timezones deduced with help from:
+ // http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+ 'Universal Coordinated Time' => 'UTC',
+ 'Casablanca, Monrovia' => 'Africa/Casablanca',
+ 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
+ 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London',
+ 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+ 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
+ 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+ 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
+ 'Prague, Central Europe' => 'Europe/Prague',
+ 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo',
+ 'West Central Africa' => 'Africa/Luanda', // This was a best guess
+ 'Athens, Istanbul, Minsk' => 'Europe/Athens',
+ 'Bucharest' => 'Europe/Bucharest',
+ 'Cairo' => 'Africa/Cairo',
+ 'Harare, Pretoria' => 'Africa/Harare',
+ 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki',
+ 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem',
+ 'Baghdad' => 'Asia/Baghdad',
+ 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait',
+ 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+ 'East Africa, Nairobi' => 'Africa/Nairobi',
+ 'Tehran' => 'Asia/Tehran',
+ 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess
+ 'Baku, Tbilisi, Yerevan' => 'Asia/Baku',
+ 'Kabul' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
+ 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
+ 'Kathmandu, Nepal' => 'Asia/Kathmandu',
+ 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
+ 'Astana, Dhaka' => 'Asia/Dhaka',
+ 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
+ 'Rangoon' => 'Asia/Rangoon',
+ 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+ 'Krasnoyarsk' => 'Asia/Krasnoyarsk',
+ 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
+ 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk',
+ 'Kuala Lumpur, Singapore' => 'Asia/Singapore',
+ 'Perth, Western Australia' => 'Australia/Perth',
+ 'Taipei' => 'Asia/Taipei',
+ 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+ 'Seoul, Korea Standard time' => 'Asia/Seoul',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'Adelaide, Central Australia' => 'Australia/Adelaide',
+ 'Darwin' => 'Australia/Darwin',
+ 'Brisbane, East Australia' => 'Australia/Brisbane',
+ 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney',
+ 'Guam, Port Moresby' => 'Pacific/Guam',
+ 'Hobart, Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan',
+ 'Auckland, Wellington' => 'Pacific/Auckland',
+ 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji',
+ 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu',
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde Is.' => 'Atlantic/Cape_Verde',
+ 'Mid-Atlantic' => 'America/Noronha',
+ 'Brasilia' => 'America/Sao_Paulo', // Best guess
+ 'Buenos Aires' => 'America/Argentina/Buenos_Aires',
+ 'Greenland' => 'America/Godthab',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Atlantic Time (Canada)' => 'America/Halifax',
+ 'Caracas, La Paz' => 'America/Caracas',
+ 'Santiago' => 'America/Santiago',
+ 'Bogota, Lima, Quito' => 'America/Bogota',
+ 'Eastern Time (US & Canada)' => 'America/New_York',
+ 'Indiana (East)' => 'America/Indiana/Indianapolis',
+ 'Central America' => 'America/Guatemala',
+ 'Central Time (US & Canada)' => 'America/Chicago',
+ 'Mexico City, Tegucigalpa' => 'America/Mexico_City',
+ 'Saskatchewan' => 'America/Edmonton',
+ 'Arizona' => 'America/Phoenix',
+ 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess
+ 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess
+ 'Alaska' => 'America/Anchorage',
+ 'Hawaii' => 'Pacific/Honolulu',
+ 'Midway Island, Samoa' => 'Pacific/Midway',
+ 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein',
+
+ // The following list are timezone names that could be generated by
+ // Lotus / Domino
+ 'Dateline' => 'Etc/GMT-12',
+ 'Samoa' => 'Pacific/Apia',
+ 'Hawaiian' => 'Pacific/Honolulu',
+ 'Alaskan' => 'America/Anchorage',
+ 'Pacific' => 'America/Los_Angeles',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Mexico Standard Time 2' => 'America/Chihuahua',
+ 'Mountain' => 'America/Denver',
+ 'Mountain Standard Time' => 'America/Chihuahua',
+ 'US Mountain' => 'America/Phoenix',
+ 'Canada Central' => 'America/Edmonton',
+ 'Central America' => 'America/Guatemala',
+ 'Central' => 'America/Chicago',
+ 'Central Standard Time' => 'America/Mexico_City',
+ 'Mexico' => 'America/Mexico_City',
+ 'Eastern' => 'America/New_York',
+ 'SA Pacific' => 'America/Bogota',
+ 'US Eastern' => 'America/Indiana/Indianapolis',
+ 'Venezuela' => 'America/Caracas',
+ 'Atlantic' => 'America/Halifax',
+ 'Central Brazilian' => 'America/Manaus',
+ 'Pacific SA' => 'America/Santiago',
+ 'SA Western' => 'America/La_Paz',
+ 'Newfoundland' => 'America/St_Johns',
+ 'Argentina' => 'America/Argentina/Buenos_Aires',
+ 'E. South America' => 'America/Belem',
+ 'Greenland' => 'America/Godthab',
+ 'Montevideo' => 'America/Montevideo',
+ 'SA Eastern' => 'America/Belem',
+ 'Mid-Atlantic' => 'Etc/GMT-2',
+ 'Azores' => 'Atlantic/Azores',
+ 'Cape Verde' => 'Atlantic/Cape_Verde',
+ 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
+ 'Morocco' => 'Africa/Casablanca',
+ 'Central Europe' => 'Europe/Prague',
+ 'Central European' => 'Europe/Sarajevo',
+ 'Romance' => 'Europe/Paris',
+ 'W. Central Africa' => 'Africa/Lagos', // Best guess
+ 'W. Europe' => 'Europe/Amsterdam',
+ 'E. Europe' => 'Europe/Minsk',
+ 'Egypt' => 'Africa/Cairo',
+ 'FLE' => 'Europe/Helsinki',
+ 'GTB' => 'Europe/Athens',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jordan' => 'Asia/Amman',
+ 'Middle East' => 'Asia/Beirut',
+ 'Namibia' => 'Africa/Windhoek',
+ 'South Africa' => 'Africa/Harare',
+ 'Arab' => 'Asia/Kuwait',
+ 'Arabic' => 'Asia/Baghdad',
+ 'E. Africa' => 'Africa/Nairobi',
+ 'Georgian' => 'Asia/Tbilisi',
+ 'Russian' => 'Europe/Moscow',
+ 'Iran' => 'Asia/Tehran',
+ 'Arabian' => 'Asia/Muscat',
+ 'Armenian' => 'Asia/Yerevan',
+ 'Azerbijan' => 'Asia/Baku',
+ 'Caucasus' => 'Asia/Yerevan',
+ 'Mauritius' => 'Indian/Mauritius',
+ 'Afghanistan' => 'Asia/Kabul',
+ 'Ekaterinburg' => 'Asia/Yekaterinburg',
+ 'Pakistan' => 'Asia/Karachi',
+ 'West Asia' => 'Asia/Tashkent',
+ 'India' => 'Asia/Calcutta',
+ 'Sri Lanka' => 'Asia/Colombo',
+ 'Nepal' => 'Asia/Kathmandu',
+ 'Central Asia' => 'Asia/Dhaka',
+ 'N. Central Asia' => 'Asia/Almaty',
+ 'Myanmar' => 'Asia/Rangoon',
+ 'North Asia' => 'Asia/Krasnoyarsk',
+ 'SE Asia' => 'Asia/Bangkok',
+ 'China' => 'Asia/Shanghai',
+ 'North Asia East' => 'Asia/Irkutsk',
+ 'Singapore' => 'Asia/Singapore',
+ 'Taipei' => 'Asia/Taipei',
+ 'W. Australia' => 'Australia/Perth',
+ 'Korea' => 'Asia/Seoul',
+ 'Tokyo' => 'Asia/Tokyo',
+ 'Yakutsk' => 'Asia/Yakutsk',
+ 'AUS Central' => 'Australia/Darwin',
+ 'Cen. Australia' => 'Australia/Adelaide',
+ 'AUS Eastern' => 'Australia/Sydney',
+ 'E. Australia' => 'Australia/Brisbane',
+ 'Tasmania' => 'Australia/Hobart',
+ 'Vladivostok' => 'Asia/Vladivostok',
+ 'West Pacific' => 'Pacific/Guam',
+ 'Central Pacific' => 'Asia/Magadan',
+ 'Fiji' => 'Pacific/Fiji',
+ 'New Zealand' => 'Pacific/Auckland',
+ 'Tonga' => 'Pacific/Tongatapu',
+ );
+
+ /**
+ * List of microsoft exchange timezone ids.
+ *
+ * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
+ */
+ public static $microsoftExchangeMap = array(
+ 0 => 'UTC',
+ 31 => 'Africa/Casablanca',
+
+ // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
+ // I'm not even kidding.. We handle this special case in the
+ // getTimeZone method.
+ 2 => 'Europe/Lisbon',
+ 1 => 'Europe/London',
+ 4 => 'Europe/Berlin',
+ 6 => 'Europe/Prague',
+ 3 => 'Europe/Paris',
+ 69 => 'Africa/Luanda', // This was a best guess
+ 7 => 'Europe/Athens',
+ 5 => 'Europe/Bucharest',
+ 49 => 'Africa/Cairo',
+ 50 => 'Africa/Harare',
+ 59 => 'Europe/Helsinki',
+ 27 => 'Asia/Jerusalem',
+ 26 => 'Asia/Baghdad',
+ 74 => 'Asia/Kuwait',
+ 51 => 'Europe/Moscow',
+ 56 => 'Africa/Nairobi',
+ 25 => 'Asia/Tehran',
+ 24 => 'Asia/Muscat', // Best guess
+ 54 => 'Asia/Baku',
+ 48 => 'Asia/Kabul',
+ 58 => 'Asia/Yekaterinburg',
+ 47 => 'Asia/Karachi',
+ 23 => 'Asia/Calcutta',
+ 62 => 'Asia/Kathmandu',
+ 46 => 'Asia/Almaty',
+ 71 => 'Asia/Dhaka',
+ 66 => 'Asia/Colombo',
+ 61 => 'Asia/Rangoon',
+ 22 => 'Asia/Bangkok',
+ 64 => 'Asia/Krasnoyarsk',
+ 45 => 'Asia/Shanghai',
+ 63 => 'Asia/Irkutsk',
+ 21 => 'Asia/Singapore',
+ 73 => 'Australia/Perth',
+ 75 => 'Asia/Taipei',
+ 20 => 'Asia/Tokyo',
+ 72 => 'Asia/Seoul',
+ 70 => 'Asia/Yakutsk',
+ 19 => 'Australia/Adelaide',
+ 44 => 'Australia/Darwin',
+ 18 => 'Australia/Brisbane',
+ 76 => 'Australia/Sydney',
+ 43 => 'Pacific/Guam',
+ 42 => 'Australia/Hobart',
+ 68 => 'Asia/Vladivostok',
+ 41 => 'Asia/Magadan',
+ 17 => 'Pacific/Auckland',
+ 40 => 'Pacific/Fiji',
+ 67 => 'Pacific/Tongatapu',
+ 29 => 'Atlantic/Azores',
+ 53 => 'Atlantic/Cape_Verde',
+ 30 => 'America/Noronha',
+ 8 => 'America/Sao_Paulo', // Best guess
+ 32 => 'America/Argentina/Buenos_Aires',
+ 60 => 'America/Godthab',
+ 28 => 'America/St_Johns',
+ 9 => 'America/Halifax',
+ 33 => 'America/Caracas',
+ 65 => 'America/Santiago',
+ 35 => 'America/Bogota',
+ 10 => 'America/New_York',
+ 34 => 'America/Indiana/Indianapolis',
+ 55 => 'America/Guatemala',
+ 11 => 'America/Chicago',
+ 37 => 'America/Mexico_City',
+ 36 => 'America/Edmonton',
+ 38 => 'America/Phoenix',
+ 12 => 'America/Denver', // Best guess
+ 13 => 'America/Los_Angeles', // Best guess
+ 14 => 'America/Anchorage',
+ 15 => 'Pacific/Honolulu',
+ 16 => 'Pacific/Midway',
+ 39 => 'Pacific/Kwajalein',
+ );
+
+ /**
+ * This method will try to find out the correct timezone for an iCalendar
+ * date-time value.
+ *
+ * You must pass the contents of the TZID parameter, as well as the full
+ * calendar.
+ *
+ * If the lookup fails, this method will return the default PHP timezone
+ * (as configured using date_default_timezone_set, or the date.timezone ini
+ * setting).
+ *
+ * Alternatively, if $failIfUncertain is set to true, it will throw an
+ * exception if we cannot accurately determine the timezone.
+ *
+ * @param string $tzid
+ * @param OldSabre\VObject\Component $vcalendar
+ * @return DateTimeZone
+ */
+ static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) {
+
+ // First we will just see if the tzid is a support timezone identifier.
+ try {
+ return new \DateTimeZone($tzid);
+ } catch (\Exception $e) {
+ }
+
+ // Next, we check if the tzid is somewhere in our tzid map.
+ if (isset(self::$map[$tzid])) {
+ return new \DateTimeZone(self::$map[$tzid]);
+ }
+
+ // Maybe the author was hyper-lazy and just included an offset. We
+ // support it, but we aren't happy about it.
+ if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
+ return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0'));
+ }
+
+ if ($vcalendar) {
+
+ // If that didn't work, we will scan VTIMEZONE objects
+ foreach($vcalendar->select('VTIMEZONE') as $vtimezone) {
+
+ if ((string)$vtimezone->TZID === $tzid) {
+
+ // Some clients add 'X-LIC-LOCATION' with the olson name.
+ if (isset($vtimezone->{'X-LIC-LOCATION'})) {
+
+ $lic = (string)$vtimezone->{'X-LIC-LOCATION'};
+
+ // Libical generators may specify strings like
+ // "SystemV/EST5EDT". For those we must remove the
+ // SystemV part.
+ if (substr($lic,0,8)==='SystemV/') {
+ $lic = substr($lic,8);
+ }
+
+ try {
+ return new \DateTimeZone($lic);
+ } catch (\Exception $e) {
+ }
+
+ }
+ // Microsoft may add a magic number, which we also have an
+ // answer for.
+ if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
+ $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value;
+
+ // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
+ if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) {
+ return new \DateTimeZone('Europe/Sarajevo');
+ }
+
+ if (isset(self::$microsoftExchangeMap[$cdoId])) {
+ return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+ if ($failIfUncertain) {
+ throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid);
+ }
+
+ // If we got all the way here, we default to UTC.
+ return new \DateTimeZone(date_default_timezone_get());
+
+ }
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php
new file mode 100644
index 0000000..8de8885
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace OldSabre\VObject;
+
+/**
+ * This class contains the version number for the VObject package
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Version {
+
+ /**
+ * Full version number
+ */
+ const VERSION = '2.1.0';
+
+ /**
+ * Stability : alpha, beta, stable
+ */
+ const STABILITY = 'stable';
+
+}
diff --git a/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php
new file mode 100644
index 0000000..d15329a
--- /dev/null
+++ b/calendar/lib/SabreDAV/vendor/oldsabre/vobject/lib/Sabre/VObject/includes.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Includes file
+ *
+ * This file includes the entire VObject library in one go.
+ * The benefit is that an autoloader is not needed, which is often faster.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+
+// Begin includes
+include __DIR__ . '/DateTimeParser.php';
+include __DIR__ . '/ElementList.php';
+include __DIR__ . '/FreeBusyGenerator.php';
+include __DIR__ . '/Node.php';
+include __DIR__ . '/Parameter.php';
+include __DIR__ . '/ParseException.php';
+include __DIR__ . '/Property.php';
+include __DIR__ . '/Reader.php';
+include __DIR__ . '/RecurrenceIterator.php';
+include __DIR__ . '/Splitter/SplitterInterface.php';
+include __DIR__ . '/StringUtil.php';
+include __DIR__ . '/TimeZoneUtil.php';
+include __DIR__ . '/Version.php';
+include __DIR__ . '/Splitter/VCard.php';
+include __DIR__ . '/Component.php';
+include __DIR__ . '/Document.php';
+include __DIR__ . '/Property/Compound.php';
+include __DIR__ . '/Property/DateTime.php';
+include __DIR__ . '/Property/MultiDateTime.php';
+include __DIR__ . '/Splitter/ICalendar.php';
+include __DIR__ . '/Component/VAlarm.php';
+include __DIR__ . '/Component/VCalendar.php';
+include __DIR__ . '/Component/VEvent.php';
+include __DIR__ . '/Component/VFreeBusy.php';
+include __DIR__ . '/Component/VJournal.php';
+include __DIR__ . '/Component/VTodo.php';
+// End includes
diff --git a/calendar/lib/caldav-client.php b/calendar/lib/caldav-client.php
new file mode 100644
index 0000000..f0f1671
--- /dev/null
+++ b/calendar/lib/caldav-client.php
@@ -0,0 +1,379 @@
+<?php
+
+/**
+ * CalDAV Client
+ *
+ * @version @package_version@
+ * @author Daniel Morlock <daniel.morlock@awesome-it.de>
+ *
+ * Copyright (C) Awesome IT GbR <info@awesome-it.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require_once (dirname(__FILE__).'/SabreDAV/vendor/autoload.php');
+
+
+class caldav_client extends OldSabre\DAV\Client
+{
+ const CLARK_GETCTAG = '{http://calendarserver.org/ns/}getctag';
+ const CLARK_GETETAG = '{DAV:}getetag';
+ const CLARK_CALDATA = '{urn:ietf:params:xml:ns:caldav}calendar-data';
+
+ private $base_uri;
+ private $path;
+ private $libvcal;
+
+ /**
+ * Default constructor for CalDAV client.
+ *
+ * @param string Caldav URI to appropriate calendar.
+ * @param string Username for HTTP basic auth.
+ * @param string Password for HTTP basic auth.
+ */
+ public function __construct($uri, $user = null, $pass = null)
+ {
+
+ // Include libvcalendar on demand ...
+ if(!class_exists("libvcalendar"))
+ require_once (dirname(__FILE__).'/../../libcalendaring/libvcalendar.php');
+
+ $this->libvcal = new libvcalendar();
+
+ $tokens = parse_url($uri);
+ $this->base_uri = $tokens['scheme']."://".$tokens['host'].($tokens['port'] ? ":".$tokens['port'] : null);
+ $this->path = $tokens['path'].($tokens['query'] ? "?".$tokens['query'] : null);
+
+ $settings = array(
+ 'baseUri' => $this->base_uri,
+ 'authType' => OldSabre\DAV\Client::AUTH_BASIC
+ );
+
+ // Forward SSL certificate setting to Sabre\DAV an onwards to CURL
+ $this->rc = rcmail::get_instance();
+ parent::setVerifyPeer($this->rc->config->get('calendar_curl_secure_ssl', true));
+
+ if ($user) $settings['userName'] = $user;
+ if ($pass) $settings['password'] = $pass;
+
+ parent::__construct($settings);
+ }
+
+ /**
+ * Fetches calendar ctag.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Retrieving_calendar_information
+ * @return Calendar ctag or null on error.
+ */
+ public function get_ctag()
+ {
+ try
+ {
+ $arr = $this->propFind($this->path, array(self::CLARK_GETCTAG));
+
+ if (isset($arr[self::CLARK_GETCTAG]))
+ return $arr[self::CLARK_GETCTAG];
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+
+ return null;
+ }
+
+ /**
+ * Fetches event etags and urls.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Finding_out_if_anything_changed
+ *
+ * @param array Optional list of relative event URL's to retrieve specific etags. If not specified, all etags of the current calendar are returned.
+ * @return array List of etag properties with keys:
+ * url: Event ical path relative to the calendar URL.
+ * etag: Current event etag.
+ */
+ public function get_etags(array $event_urls = array())
+ {
+ $etags = array();
+
+ try
+ {
+ $arr = $this->prop_report($this->path, array(self::CLARK_GETETAG), $event_urls);
+ foreach ($arr as $path => $data)
+ {
+ // Some caldav server return an empty calendar as event where etag is missing. Skip this!
+ if($data[self::CLARK_GETETAG])
+ {
+ array_push($etags, array(
+ "url" => $path,
+ "etag" => str_replace('"', null, $data[self::CLARK_GETETAG])
+ ));
+ }
+ }
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+
+ return $etags;
+ }
+
+ /**
+ * Fetches calendar events.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Downloading_objects
+ * @param array $urls = array() Optional list of event URL's to fetch. If non is specified, all
+ * events from the appropriate calendar will be fetched.
+ * @return Array hash list that maps the events URL to the appropriate event properties.
+ */
+ public function get_events($urls = array())
+ {
+ $events = array();
+
+ try
+ {
+ $vcals = $this->prop_report($this->path, array(
+ self::CLARK_GETETAG,
+ self::CLARK_CALDATA
+ ), $urls);
+
+ foreach ($vcals as $path => $response)
+ {
+ $vcal = $response[self::CLARK_CALDATA];
+ foreach ($this->libvcal->import($vcal) as $event) {
+ $events[$path] = $event;
+ }
+ }
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return $events;
+ }
+
+ /**
+ * Does a REPORT request
+ *
+ * @param string $url
+ * @param array $properties List of requested properties must be specified as an array, in clark
+ * notation.
+ * @param array $event_urls If specified, a multiget report request will be initiated with the
+ * specified event urls.
+ * @param int $depth = 1 Depth should be either 0 or 1. A depth of 1 will cause a request to be
+ * made to the server to also return all child resources.
+ * @return array Hash with ics event path as key and a hash array with properties and appropriate values.
+ */
+ public function prop_report($url, array $properties, array $event_urls = array(), $depth = 1)
+ {
+ $parent_tag = sizeof($event_urls) > 0 ? "c:calendar-multiget" : "d:propfind";
+ $method = sizeof($event_urls) > 0 ? 'REPORT' : 'PROPFIND';
+
+ $body = '<?xml version="1.0"?>'."\n".'<'.$parent_tag.' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'."\n";
+
+ $body .= ' <d:prop>'."\n";
+ foreach ($properties as $property)
+ {
+
+ list($namespace, $elementName) = OldSabre\DAV\XMLUtil::parseClarkNotation($property);
+
+ if ($namespace === 'DAV:')
+ {
+ $body .= ' <d:'.$elementName.' />'."\n";
+ }
+ else
+ {
+ $body .= ' <x:'.$elementName.' xmlns:x="'.$namespace.'"/>'."\n";
+ }
+ }
+ $body .= ' </d:prop>'."\n";
+
+ // http://tools.ietf.org/html/rfc4791#page-90
+ // http://www.bedework.org/trac/bedework/wiki/Bedework/DevDocs/Filters
+ /*
+ if($start && $end)
+ {
+ $body.= ' <c:filter>'."\n".
+ ' <c:comp-filter name="VCALENDAR">'."\n".
+ ' <c:comp-filter name="VEVENT">'."\n".
+ ' <c:time-range start="'.$start.'" end="'.$end.'" />'."\n".
+ ' </c:comp-filter>'."\n".
+ ' </c:comp-filter>'."\n".
+ ' </c:filter>' . "\n";
+ }
+ */
+
+ foreach ($event_urls as $event_url)
+ {
+ $body .= '<d:href>'.$event_url.'</d:href>'."\n";
+ }
+
+ $body .= '</'.$parent_tag.'>';
+
+ $response = $this->request($method, $url, $body, array(
+ 'Depth' => $depth,
+ 'Content-Type' => 'application/xml'
+ ));
+
+ $result = $this->parseMultiStatus($response['body']);
+
+ // If depth was 0, we only return the top item
+ if ($depth === 0)
+ {
+ reset($result);
+ $result = current($result);
+ return isset($result[200]) ? $result[200] : array();
+ }
+
+ $new_result = array();
+ foreach ($result as $href => $status_list)
+ {
+ $new_result[$href] = isset($status_list[200]) ? $status_list[200] : array();
+ }
+
+ return $new_result;
+ }
+
+ /**
+ * Updates or creates a calendar event.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Updating_a_calendar_object
+ * @param string Event ics path for the event.
+ * @param array Hash array with event properties.
+ * @param string Current event etag to match against server data. Pass null for new events.
+ * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error.
+ */
+ public function put_event($path, $event, $etag = null)
+ {
+ try
+ {
+ $headers = array("Content-Type" => "text/calendar; charset=utf-8");
+ if ($etag) $headers["If-Match"] = '"'.$etag.'"';
+
+ // Temporarily disable error reporting since libvcal seems not checking array key properly.
+ // TODO: Remove this todo if we could ensure that those errors come not from incomplete event properties.
+ $err_rep = error_reporting(E_ERROR);
+ $vcal = $this->libvcal->export(array($event));
+ if (is_array($vcal))
+ $vcal = array_shift($vcal);
+ error_reporting($err_rep);
+
+ $response = $this->request('PUT', $path, $vcal, $headers);
+
+ // Following http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Creating_a_calendar_object, the
+ // caldav server must not always return the new etag.
+
+ return $response["statusCode"] == 201 || // 201 (created, successfully created)
+ $response["statusCode"] == 204; // 204 (no content, successfully updated)
+ }
+ catch(OldSabre\DAV\Exception\PreconditionFailed $err)
+ {
+ // Event tag not up to date, must be updated first ...
+ return -1;
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return false;
+ }
+
+ /**
+ * Removes event of given URL.
+ *
+ * @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Deleting_a_calendar_object
+ * @param string Event ics path for the event.
+ * @param string Current event etag to match against server data. Pass null to force removing the event.
+ * @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error.
+ **/
+ public function remove_event($path, $etag = null)
+ {
+ try
+ {
+ $headers = array("Content-Type" => "text/calendar; charset=utf-8");
+ if ($etag) $headers["If-Match"] = '"'.$etag.'"';
+
+ $response = $this->request('DELETE', $path, null, $headers);
+ return $response["statusCode"] == 204 || // 204 (no content, successfully deleted)
+ $response["statusCode"] == 200; // 200 (OK, successfully deleted)
+ }
+ catch(OldSabre\DAV\Exception\PreconditionFailed $err)
+ {
+ // Event tag not up to date, must be updated first ...
+ return -1;
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return false;
+ }
+
+ /**
+ * Make a propFind query to caldav server
+ * @param string $path absolute or relative URL to Resource
+ * @param array $props list of properties to use for the query. Properties must have clark-notation.
+ * @param int $depth 0 means no recurse while 1 means recurse
+ * @return array
+ */
+ public function prop_find($path, $props, $depth)
+ {
+ try {
+ $response = $this->propFind($path, $props, $depth);
+ }
+ catch(OldSabre\DAV\Exception $err)
+ {
+ rcube::raise_error(array(
+ 'code' => $err->getHTTPCode(),
+ 'type' => 'DAV',
+ 'file' => $err->getFile(),
+ 'line' => $err->getLine(),
+ 'message' => $err->getMessage()
+ ), true, false);
+ }
+ return $response;
+ }
+};
+?>
diff --git a/calendar/lib/calendar_itip.php b/calendar/lib/calendar_itip.php
new file mode 100644
index 0000000..e2a2402
--- /dev/null
+++ b/calendar/lib/calendar_itip.php
@@ -0,0 +1,240 @@
+<?php
+
+require_once realpath(__DIR__ . '/../../libcalendaring/lib/libcalendaring_itip.php');
+
+/**
+ * iTIP functions for the Calendar plugin
+ *
+ * Class providing functionality to manage iTIP invitations
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @package @package_name@
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class calendar_itip extends libcalendaring_itip
+{
+ /**
+ * Constructor to set text domain to calendar
+ */
+ function __construct($plugin, $domain = 'calendar')
+ {
+ parent::__construct($plugin, $domain);
+
+ $this->db_itipinvitations = $this->rc->db->table_name('itipinvitations', true);
+ }
+
+ /**
+ * Handler for calendar/itip-status requests
+ */
+ public function get_itip_status($event, $existing = null)
+ {
+ $status = parent::get_itip_status($event, $existing);
+
+ // don't ask for deleting events when declining
+ if ($this->rc->config->get('kolab_invitation_calendars'))
+ $status['saved'] = false;
+
+ return $status;
+ }
+
+ /**
+ * Find invitation record by token
+ *
+ * @param string Invitation token
+ * @return mixed Invitation record as hash array or False if not found
+ */
+ public function get_invitation($token)
+ {
+ if ($parts = $this->decode_token($token)) {
+ $result = $this->rc->db->query("SELECT * FROM $this->db_itipinvitations WHERE `token` = ?", $parts['base']);
+ if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+ $rec['event'] = unserialize($rec['event']);
+ $rec['attendee'] = $parts['attendee'];
+ return $rec;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the attendee status of the given invitation record
+ *
+ * @param array Invitation record as fetched with calendar_itip::get_invitation()
+ * @param string Attendee email address
+ * @param string New attendee status
+ */
+ public function update_invitation($invitation, $email, $newstatus)
+ {
+ if (is_string($invitation))
+ $invitation = $this->get_invitation($invitation);
+
+ if ($invitation['token'] && $invitation['event']) {
+ // update attendee record in event data
+ foreach ($invitation['event']['attendees'] as $i => $attendee) {
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $attendee;
+ }
+ else if ($attendee['email'] == $email) {
+ // nothing to be done here
+ if ($attendee['status'] == $newstatus)
+ return true;
+
+ $invitation['event']['attendees'][$i]['status'] = $newstatus;
+ $this->sender = $attendee;
+ }
+ }
+ $invitation['event']['changed'] = new DateTime();
+
+ // send iTIP REPLY message to organizer
+ if ($organizer) {
+ $status = strtolower($newstatus);
+ if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+ $this->rc->output->command('display_message', $this->plugin->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
+ else
+ $this->rc->output->command('display_message', $this->plugin->gettext('itipresponseerror'), 'error');
+ }
+
+ // update record in DB
+ $query = $this->rc->db->query(
+ "UPDATE $this->db_itipinvitations
+ SET `event` = ?
+ WHERE `token` = ?",
+ self::serialize_event($invitation['event']),
+ $invitation['token']
+ );
+
+ if ($this->rc->db->affected_rows($query))
+ return true;
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Create iTIP invitation token for later replies via URL
+ *
+ * @param array Hash array with event properties
+ * @param string Attendee email address
+ * @return string Invitation token
+ */
+ public function store_invitation($event, $attendee)
+ {
+ static $stored = array();
+
+ if (!$event['uid'] || !$attendee)
+ return false;
+
+ // generate token for this invitation
+ $token = $this->generate_token($event, $attendee);
+ $base = substr($token, 0, 40);
+
+ // already stored this
+ if ($stored[$base])
+ return $token;
+
+ // delete old entry
+ $this->rc->db->query("DELETE FROM $this->db_itipinvitations WHERE `token` = ?", $base);
+
+ $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+
+ $query = $this->rc->db->query(
+ "INSERT INTO $this->db_itipinvitations
+ (`token`, `event_uid`, `user_id`, `event`, `expires`)
+ VALUES(?, ?, ?, ?, ?)",
+ $base,
+ $event_uid,
+ $this->rc->user->ID,
+ self::serialize_event($event),
+ date('Y-m-d H:i:s', $event['end']->format('U') + 86400 * 2)
+ );
+
+ if ($this->rc->db->affected_rows($query)) {
+ $stored[$base] = 1;
+ return $token;
+ }
+
+ return false;
+ }
+
+ /**
+ * Mark invitations for the given event as cancelled
+ *
+ * @param array Hash array with event properties
+ */
+ public function cancel_itip_invitation($event)
+ {
+ $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+
+ // flag invitation record as cancelled
+ $this->rc->db->query(
+ "UPDATE $this->db_itipinvitations
+ SET `cancelled` = 1
+ WHERE `event_uid` = ? AND `user_id` = ?",
+ $event_uid,
+ $this->rc->user->ID
+ );
+ }
+
+ /**
+ * Generate an invitation request token for the given event and attendee
+ *
+ * @param array Event hash array
+ * @param string Attendee email address
+ */
+ public function generate_token($event, $attendee)
+ {
+ $event_uid = $event['uid'] . ($event['_instance'] ? '-' . $event['_instance'] : '');
+ $base = sha1($event_uid . ';' . $this->rc->user->ID);
+ $mail = base64_encode($attendee);
+ $hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6);
+
+ return "$base.$mail.$hash";
+ }
+
+ /**
+ * Decode the given iTIP request token and return its parts
+ *
+ * @param string Request token to decode
+ * @return mixed Hash array with parts or False if invalid
+ */
+ public function decode_token($token)
+ {
+ list($base, $mail, $hash) = explode('.', $token);
+
+ // validate and return parts
+ if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) {
+ return array('base' => $base, 'attendee' => base64_decode($mail));
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper method to serialize the given event for storing in invitations table
+ */
+ private static function serialize_event($event)
+ {
+ $ev = $event;
+ $ev['description'] = abbreviate_string($ev['description'], 100);
+ unset($ev['attachments']);
+ return serialize($ev);
+ }
+
+}
diff --git a/calendar/lib/calendar_recurrence.php b/calendar/lib/calendar_recurrence.php
new file mode 100644
index 0000000..44d5b08
--- /dev/null
+++ b/calendar/lib/calendar_recurrence.php
@@ -0,0 +1,88 @@
+<?php
+
+require_once realpath(__DIR__ . '/../../libcalendaring/lib/libcalendaring_recurrence.php');
+
+/**
+ * Recurrence computation class for the Calendar plugin
+ *
+ * Uitility class to compute instances of recurring events.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class calendar_recurrence extends libcalendaring_recurrence
+{
+ private $event;
+ private $duration;
+
+ /**
+ * Default constructor
+ *
+ * @param object calendar The calendar plugin instance
+ * @param array The event object to operate on
+ */
+ function __construct($cal, $event)
+ {
+ parent::__construct($cal->lib);
+
+ $this->event = $event;
+
+ if (is_object($event['start']) && is_object($event['end']))
+ $this->duration = $event['start']->diff($event['end']);
+
+ $event['start']->_dateonly |= $event['allday'];
+ $this->init($event['recurrence'], $event['start']);
+ }
+
+ /**
+ * Alias of libcalendaring_recurrence::next()
+ *
+ * @return mixed DateTime object or False if recurrence ended
+ */
+ public function next_start()
+ {
+ return $this->next();
+ }
+
+ /**
+ * Get the next recurring instance of this event
+ *
+ * @return mixed Array with event properties or False if recurrence ended
+ */
+ public function next_instance()
+ {
+ if ($next_start = $this->next()) {
+ $next = $this->event;
+ $next['start'] = $next_start;
+
+ if ($this->duration) {
+ $next['end'] = clone $next_start;
+ $next['end']->add($this->duration);
+ }
+
+ $next['recurrence_date'] = clone $next_start;
+ $next['_instance'] = libcalendaring::recurrence_instance_identifier($next);
+
+ unset($next['_formatobj']);
+
+ return $next;
+ }
+
+ return false;
+ }
+
+}
diff --git a/calendar/lib/calendar_ui.php b/calendar/lib/calendar_ui.php
new file mode 100644
index 0000000..aa74f52
--- /dev/null
+++ b/calendar/lib/calendar_ui.php
@@ -0,0 +1,918 @@
+<?php
+/**
+ * User Interface class for the Calendar plugin
+ *
+ * @author Lazlo Westerhof <hello@lazlo.me>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class calendar_ui
+{
+ private $rc;
+ private $cal;
+ private $ready = false;
+ public $screen;
+
+ function __construct($cal)
+ {
+ $this->cal = $cal;
+ $this->rc = $cal->rc;
+ $this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other';
+ }
+
+ /**
+ * Calendar UI initialization and requests handlers
+ */
+ public function init()
+ {
+ if ($this->ready) // already done
+ return;
+
+ // add taskbar button
+ $this->cal->add_button(array(
+ 'command' => 'calendar',
+ 'class' => 'button-calendar',
+ 'classsel' => 'button-calendar button-selected',
+ 'innerclass' => 'button-inner',
+ 'label' => 'calendar.calendar',
+ ), 'taskbar');
+
+ // load basic client script
+ $this->cal->include_script('calendar_base.js');
+
+ $skin_path = $this->cal->local_skin_path();
+ $this->cal->include_stylesheet($skin_path . '/calendar.css');
+
+ $this->ready = true;
+ }
+
+ /**
+ * Register handler methods for the template engine
+ */
+ public function init_templates()
+ {
+ $this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css'));
+ $this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list'));
+ $this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select'));
+ $this->cal->register_handler('plugin.identity_select', array($this, 'identity_select'));
+ $this->cal->register_handler('plugin.category_select', array($this, 'category_select'));
+ $this->cal->register_handler('plugin.status_select', array($this, 'status_select'));
+ $this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select'));
+ $this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
+ $this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
+ $this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
+ $this->cal->register_handler('plugin.recurrence_form', array($this->cal->lib, 'recurrence_form'));
+ $this->cal->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
+ $this->cal->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
+ $this->cal->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
+ $this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
+ $this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
+ $this->cal->register_handler('plugin.resources_form', array($this, 'resources_form'));
+ $this->cal->register_handler('plugin.resources_list', array($this, 'resources_list'));
+ $this->cal->register_handler('plugin.resources_searchform', array($this, 'resources_search_form'));
+ $this->cal->register_handler('plugin.resource_info', array($this, 'resource_info'));
+ $this->cal->register_handler('plugin.resource_calendar', array($this, 'resource_calendar'));
+ $this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
+ $this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
+ $this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
+ $this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
+ $this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
+ $this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
+ $this->cal->register_handler('plugin.events_export_form', array($this, 'events_export_form'));
+ $this->cal->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
+ $this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
+ $this->cal->register_handler('plugin.calendar_create_menu', array($this, 'calendar_create_menu'));
+ }
+
+ /**
+ * Adds CSS stylesheets to the page header
+ */
+ public function addCSS()
+ {
+ $skin_path = $this->cal->local_skin_path();
+ $this->cal->include_stylesheet($skin_path . '/fullcalendar.css');
+ }
+
+ /**
+ * Adds JS files to the page header
+ */
+ public function addJS()
+ {
+ $this->cal->include_script('calendar_ui.js');
+ $this->cal->include_script('lib/js/fullcalendar.js');
+ $this->rc->output->include_script('treelist.js');
+
+ // include kolab folderlist widget if available
+ if (in_array('libkolab', $this->cal->api->loaded_plugins())) {
+ $this->cal->api->include_script('libkolab/js/folderlist.js');
+ $this->cal->api->include_script('libkolab/js/audittrail.js');
+ }
+
+ jqueryui::miniColors();
+ }
+
+ /**
+ *
+ */
+ function calendar_css($attrib = array())
+ {
+ $mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
+ $css = "\n";
+
+ foreach ($this->cal->get_drivers() as $name => $driver) {
+ $categories = $driver->list_categories();
+
+ foreach ((array)$categories as $class => $color) {
+ if (empty($color))
+ continue;
+
+ $class = 'cat-' . asciiwords(strtolower($class), true);
+ $css .= ".$class { color: #$color }\n";
+ if ($mode > 0) {
+ if ($mode == 2) {
+ $css .= ".fc-event-$class .fc-event-bg {";
+ $css .= " opacity: 0.9;";
+ $css .= " filter: alpha(opacity=90);";
+ } else {
+ $css .= ".fc-event-$class.fc-event-skin, ";
+ $css .= ".fc-event-$class .fc-event-skin, ";
+ $css .= ".fc-event-$class .fc-event-inner {";
+ }
+ $css .= " background-color: #" . $color . ";";
+ if ($mode % 2)
+ $css .= " border-color: #$color;";
+ $css .= "}\n";
+ }
+ }
+
+ $calendars = $driver->list_calendars();
+ foreach ((array)$calendars as $id => $prop) {
+ if (!$prop['color'])
+ continue;
+ $css .= $this->calendar_css_classes($id, $prop, $mode);
+ }
+ }
+
+ return html::tag('style', array('type' => 'text/css'), $css);
+ }
+
+ /**
+ *
+ */
+ public function calendar_css_classes($id, $prop, $mode)
+ {
+ $color = $prop['color'];
+ $class = 'cal-' . asciiwords($id, true);
+ $css .= "li .$class, #eventshow .$class { color: #$color }\n";
+
+ if ($mode != 1) {
+ if ($mode == 3) {
+ $css .= ".fc-event-$class .fc-event-bg {";
+ $css .= " opacity: 0.9;";
+ $css .= " filter: alpha(opacity=90);";
+ }
+ else {
+ $css .= ".fc-event-$class, ";
+ $css .= ".fc-event-$class .fc-event-inner {";
+ }
+ if (!$attrib['printmode']) // FIXME
+ $css .= " background-color: #$color;";
+ if ($mode % 2 == 0)
+ $css .= " border-color: #$color;";
+ $css .= "}\n";
+ }
+
+ return $css . ".$class .handle { background-color: #$color; }\n";
+ }
+
+ /**
+ *
+ */
+ function calendar_list($attrib = array())
+ {
+ $html = '';
+ $jsenv = array();
+ $tree = true;
+ // TODO: Check whether get_calendars() exists. Original $calendars = $this->cal->driver->list_calendars(0, $tree);
+ $calendars = $this->cal->get_calendars(false, false, $tree);
+
+ // walk folder tree
+ if (is_object($tree)) {
+ $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
+
+ // append birthdays calendar which isn't part of $tree
+ if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
+ $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
+ }
+ else {
+ $calendars = array(); // clear array for flat listing
+ }
+ }
+ else {
+ // fall-back to flat folder listing
+ $attrib['class'] .= ' flat';
+ }
+
+ foreach ((array)$calendars as $id => $prop) {
+ if ($attrib['activeonly'] && !$prop['active'])
+ continue;
+
+ $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
+ $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
+ );
+ }
+
+ $this->rc->output->set_env('calendars', $jsenv);
+ $this->rc->output->add_gui_object('calendarslist', $attrib['id']);
+
+ return html::tag('ul', $attrib, $html, html::$common_attrib);
+ }
+
+ /**
+ * Return html for a structured list <ul> for the folder tree
+ */
+ public function list_tree_html($node, $data, &$jsenv, $attrib)
+ {
+ $out = '';
+ foreach ($node->children as $folder) {
+ $id = $folder->id;
+ $prop = $data[$id];
+ $is_collapsed = false; // TODO: determine this somehow?
+
+ $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+
+ if (!empty($folder->children)) {
+ $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+ $this->list_tree_html($folder, $data, $jsenv, $attrib));
+ }
+
+ if (strlen($content)) {
+ $out .= html::tag('li', array(
+ 'id' => 'rcmlical' . rcube_utils::html_identifier($id),
+ 'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+ ),
+ $content);
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Helper method to build a calendar list item (HTML content and js data)
+ */
+ public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
+ {
+ // enrich calendar properties with settings from the driver
+ if (!$prop['virtual']) {
+ unset($prop['user_id']);
+ $driver = $this->cal->get_driver_by_cal($id);
+ $prop['alarms'] = $driver->alarms;
+ $prop['attendees'] = $driver->attendees;
+ $prop['freebusy'] = $driver->freebusy;
+ $prop['attachments'] = $driver->attachments;
+ $prop['undelete'] = $driver->undelete;
+ $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+
+ $jsenv[$id] = $prop;
+ }
+
+ $classes = array('calendar', 'cal-' . asciiwords($id, true));
+ $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+ html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+ if ($prop['virtual'])
+ $classes[] = 'virtual';
+ else if (!$prop['editable'])
+ $classes[] = 'readonly';
+ if ($prop['subscribed'])
+ $classes[] = 'subscribed';
+ if ($prop['subscribed'] === 2)
+ $classes[] = 'partial';
+ if ($prop['class'])
+ $classes[] = $prop['class'];
+
+ $content = '';
+ if (!$activeonly || $prop['active']) {
+ $label_id = 'cl:' . $id;
+ $content = html::div(join(' ', $classes),
+ html::span(array('class' => 'calname', 'id' => $label_id, 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
+ ($prop['virtual'] ? '' :
+ html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id), '') .
+ html::span('actions',
+ ($prop['removable'] ? html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->cal->gettext('removelist')), ' ') : '') .
+ html::a(array('href' => '#', 'class' => 'quickview', 'title' => $this->cal->gettext('quickview'), 'role' => 'checkbox', 'aria-checked' => 'false'), '') .
+ (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') : '')
+ ) .
+ html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), '&nbsp;')
+ )
+ );
+ }
+
+ return $content;
+ }
+
+ /**
+ *
+ */
+ function angenda_options($attrib = array())
+ {
+ $attrib += array('id' => 'agendaoptions');
+ $attrib['style'] .= 'display:none';
+
+ $select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange'));
+ $select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), $days); // FIXME
+ foreach (array(2,5,7,14,30,60,90,180,365) as $days)
+ $select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
+
+ $html = html::label('agenda-listrange', $this->cal->gettext('listrange'));
+ $html .= $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']));
+
+ $select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections'));
+ $select_sections->add('---', '');
+ foreach (array('day' => 'libcalendaring.days', 'week' => 'libcalendaring.weeks', 'month' => 'libcalendaring.months', 'smart' => 'calendar.smartsections') as $val => $label)
+ $select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->rc->gettext($label))), $val);
+
+ $html .= html::span('spacer', '&nbsp;');
+ $html .= html::label('agenda-listsections', $this->cal->gettext('listsections'));
+ $html .= $select_sections->show($this->rc->config->get('calendar_agenda_sections', $this->cal->defaults['calendar_agenda_sections']));
+
+ return html::div($attrib, $html);
+ }
+
+ /**
+ * Render a HTML select box for calendar selection
+ */
+ function calendar_select($attrib = array())
+ {
+ $attrib['name'] = 'calendar';
+ $attrib['is_escaped'] = true;
+ $select = new html_select($attrib);
+
+ foreach ((array)$this->cal->get_calendars() as $id => $prop) {
+ if ($prop['editable'] || strpos($prop['rights'], 'i') !== false)
+ $select->add($prop['name'], $id);
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box for user identity selection
+ */
+ function identity_select($attrib = array())
+ {
+ $attrib['name'] = 'identity';
+ $select = new html_select($attrib);
+ $identities = $this->rc->user->list_emails();
+
+ foreach ($identities as $ident) {
+ $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box to select an event category
+ */
+ function category_select($attrib = array())
+ {
+ $attrib['name'] = 'categories';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ $keys = array();
+ foreach ($this->cal->get_drivers() as $driver) {
+ foreach((array)$driver->list_categories() as $key => $color) {
+ if ($color && !in_array($key, $keys)) {
+ $select->add($key, $key);
+ array_push($keys, $key);
+ }
+ }
+ }
+
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box for status property
+ */
+ function status_select($attrib = array())
+ {
+ $attrib['name'] = 'status';
+ $select = new html_select($attrib);
+ $select->add('---', '');
+ $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
+ $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
+ //$select->add($this->cal->gettext('tentative'), 'TENTATIVE');
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select box for free/busy/out-of-office property
+ */
+ function freebusy_select($attrib = array())
+ {
+ $attrib['name'] = 'freebusy';
+ $select = new html_select($attrib);
+ $select->add($this->cal->gettext('free'), 'free');
+ $select->add($this->cal->gettext('busy'), 'busy');
+ // out-of-office is not supported by libkolabxml (#3220)
+ // $select->add($this->cal->gettext('outofoffice'), 'outofoffice');
+ $select->add($this->cal->gettext('tentative'), 'tentative');
+ return $select->show(null);
+ }
+
+ /**
+ * Render a HTML select for event priorities
+ */
+ function priority_select($attrib = array())
+ {
+ $attrib['name'] = 'priority';
+ $select = new html_select($attrib);
+ $select->add('---', '0');
+ $select->add('1 '.$this->cal->gettext('highest'), '1');
+ $select->add('2 '.$this->cal->gettext('high'), '2');
+ $select->add('3 ', '3');
+ $select->add('4 ', '4');
+ $select->add('5 '.$this->cal->gettext('normal'), '5');
+ $select->add('6 ', '6');
+ $select->add('7 ', '7');
+ $select->add('8 '.$this->cal->gettext('low'), '8');
+ $select->add('9 '.$this->cal->gettext('lowest'), '9');
+ return $select->show(null);
+ }
+
+ /**
+ * Render HTML input for sensitivity selection
+ */
+ function sensitivity_select($attrib = array())
+ {
+ $attrib['name'] = 'sensitivity';
+ $select = new html_select($attrib);
+ $select->add($this->cal->gettext('public'), 'public');
+ $select->add($this->cal->gettext('private'), 'private');
+ $select->add($this->cal->gettext('confidential'), 'confidential');
+ return $select->show(null);
+ }
+
+ /**
+ * Render HTML form for alarm configuration
+ */
+ function alarm_select($attrib = array())
+ {
+ // Try GPC
+ $driver = $this->cal->get_driver_by_gpc(true /* quiet */);
+
+ // We assume that each calendar has equal alarm types, so fallback to default calendar is ok.
+ if(!$driver) $driver = $this->cal->get_default_driver();
+
+ return $this->cal->lib->alarm_select($attrib, $driver->alarm_types, $driver->alarm_absolute);
+ }
+
+ /**
+ *
+ */
+ function edit_attendees_notify($attrib = array())
+ {
+ $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
+ return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
+ }
+
+ /**
+ * Generate the form for recurrence settings
+ */
+ function recurring_event_warning($attrib = array())
+ {
+ $attrib['id'] = 'edit-recurring-warning';
+
+ $radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode'));
+ $form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' .
+ html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' .
+ html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' .
+ html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew'));
+
+ return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form));
+ }
+
+ /**
+ * Form for uploading and importing events
+ */
+ function events_import_form($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmImportForm';
+
+ // Get max filesize, enable upload progress bar
+ $max_filesize = rcube_upload_init();
+
+ $accept = '.ics, text/calendar, text/x-vcalendar, application/ics';
+ if (class_exists('ZipArchive', false)) {
+ $accept .= ', .zip, application/zip';
+ }
+
+ $input = new html_inputfield(array(
+ 'type' => 'file', 'name' => '_data', 'size' => $attrib['uploadfieldsize'],
+ 'accept' => $accept));
+
+ $select = new html_select(array('name' => '_range', 'id' => 'event-import-range'));
+ $select->add(array(
+ $this->cal->gettext('onemonthback'),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
+ $this->cal->gettext('all'),
+ ),
+ array('1','2','3','6','12',0));
+
+ $html = html::div('form-section',
+ html::div(null, $input->show()) .
+ html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
+ );
+
+ $html .= html::div('form-section',
+ html::label('event-import-calendar', $this->cal->gettext('calendar')) .
+ $this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar'))
+ );
+
+ $html .= html::div('form-section',
+ html::label('event-import-range', $this->cal->gettext('importrange')) .
+ $select->show(1)
+ );
+
+ $this->rc->output->add_gui_object('importform', $attrib['id']);
+ $this->rc->output->add_label('import');
+
+ return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')),
+ 'method' => "post", 'enctype' => 'multipart/form-data', 'id' => $attrib['id']),
+ $html
+ );
+ }
+
+ /**
+ * Form to select options for exporting events
+ */
+ function events_export_form($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmExportForm';
+
+ $html = html::div('form-section',
+ html::label('event-export-calendar', $this->cal->gettext('calendar')) .
+ $this->calendar_select(array('name' => 'calendar', 'id' => 'event-export-calendar'))
+ );
+
+ $select = new html_select(array('name' => 'range', 'id' => 'event-export-range'));
+ $select->add(array(
+ $this->cal->gettext('all'),
+ $this->cal->gettext('onemonthback'),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>3))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
+ $this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
+ $this->cal->gettext('customdate'),
+ ),
+ array(0,'1','2','3','6','12','custom'));
+
+ $startdate = new html_inputfield(array('name' => 'start', 'size' => 11, 'id' => 'event-export-startdate'));
+
+ $html .= html::div('form-section',
+ html::label('event-export-range', $this->cal->gettext('exportrange')) .
+ $select->show(0) .
+ html::span(array('style'=>'display:none'), $startdate->show())
+ );
+
+ $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'event-export-attachments', 'value' => 1));
+ $html .= html::div('form-section',
+ html::label('event-export-range', $this->cal->gettext('exportattachments')) .
+ $checkbox->show(1)
+ );
+
+ $this->rc->output->add_gui_object('exportform', $attrib['id']);
+
+ return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'export_events')),
+ 'method' => "post", 'id' => $attrib['id']),
+ $html
+ );
+ }
+
+ /**
+ * Generate the form for event attachments upload
+ */
+ function attachments_form($attrib = array())
+ {
+ // add ID if not given
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmUploadForm';
+
+ // Get max filesize, enable upload progress bar
+ $max_filesize = rcube_upload_init();
+
+ $button = new html_inputfield(array('type' => 'button'));
+ $input = new html_inputfield(array(
+ 'type' => 'file', 'name' => '_attachments[]',
+ 'multiple' => 'multiple', 'size' => $attrib['attachmentfieldsize']));
+
+ return html::div($attrib,
+ html::div(null, $input->show()) .
+ html::div('formbuttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
+ 'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
+ html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
+ );
+ }
+
+ /**
+ * Register UI object for HTML5 drag & drop file upload
+ */
+ function file_drop_area($attrib = array())
+ {
+ if ($attrib['id']) {
+ $this->rc->output->add_gui_object('filedrop', $attrib['id']);
+ $this->rc->output->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments'));
+ }
+ }
+
+ /**
+ * Generate HTML element for attachments list
+ */
+ function attachments_list($attrib = array())
+ {
+ if (!$attrib['id'])
+ $attrib['id'] = 'rcmAttachmentList';
+
+ $skin_path = $this->cal->local_skin_path();
+ if ($attrib['deleteicon']) {
+ $_SESSION[calendar::SESSION_KEY . '_deleteicon'] = $skin_path . $attrib['deleteicon'];
+ $this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']);
+ }
+ if ($attrib['cancelicon'])
+ $this->rc->output->set_env('cancelicon', $skin_path . $attrib['cancelicon']);
+ if ($attrib['loadingicon'])
+ $this->rc->output->set_env('loadingicon', $skin_path . $attrib['loadingicon']);
+
+ $this->rc->output->add_gui_object('attachmentlist', $attrib['id']);
+ $this->attachmentlist_id = $attrib['id'];
+
+ return html::tag('ul', $attrib, '', html::$common_attrib);
+ }
+
+ /**
+ * Handler for menu to choose the driver for calendar creation.
+ */
+ function calendar_create_menu($attrib = array())
+ {
+ $content = "";
+ foreach($this->cal->get_drivers() as $name => $driver)
+ {
+ $content .= html::tag('li', null, $this->rc->output->button(
+ array('label' => 'calendar.calendar_'.$name,
+ 'class' => 'active',
+ 'prop' => json_encode(array('driver' => $name)),
+ 'command' => 'calendar-create',
+ 'title' => 'calendar.createcalendar')));
+ }
+
+ return $content;
+ }
+
+ /**
+ * Handler for calendar form template.
+ * The form content could be overriden by the driver
+ */
+ function calendar_editform($action, $calendar = array())
+ {
+ // compose default calendar form fields
+ $input_name = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20));
+ $input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 6));
+ $driver = $this->cal->get_driver_by_gpc();
+
+ $formfields = array(
+ 'name' => array(
+ 'label' => $this->cal->gettext('name'),
+ 'value' => $input_name->show($calendar['name']),
+ 'id' => 'calendar-name',
+ ),
+ 'color' => array(
+ 'label' => $this->cal->gettext('color'),
+ 'value' => $input_color->show($calendar['color']),
+ 'id' => 'calendar-color',
+ ),
+ );
+
+ if ($driver->alarms) {
+ $checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1));
+ $formfields['showalarms'] = array(
+ 'label' => $this->cal->gettext('showalarms'),
+ 'value' => $checkbox->show($calendar['showalarms']?1:0),
+ 'id' => 'calendar-showalarms',
+ );
+ }
+
+ // allow driver to extend or replace the form content
+ return html::tag('form', array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'),
+ $driver->calendar_form($action, $calendar, $formfields)
+ );
+ }
+
+ /**
+ *
+ */
+ function attendees_list($attrib = array())
+ {
+ // add "noreply" checkbox to attendees table only
+ $invitations = strpos($attrib['id'], 'attend') !== false;
+
+ $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
+ $table = new html_table(array('cols' => 5 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
+
+ $table->add_header('role', $this->cal->gettext('role'));
+ $table->add_header('name', $this->cal->gettext($attrib['coltitle'] ?: 'attendee'));
+ $table->add_header('availability', $this->cal->gettext('availability'));
+ $table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
+ if ($invitations) {
+ $table->add_header(array('class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')),
+ $invite->show(1) . html::label('edit-attendees-invite', $this->cal->gettext('sendinvitations')));
+ }
+ $table->add_header('options', '');
+
+ // hide invite column if disabled by config
+ $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']);
+ if ($invitations && !($itip_notify & 2)) {
+ $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
+ $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css));
+ }
+
+ return $table->show($attrib);
+ }
+
+ /**
+ *
+ */
+ function attendees_form($attrib = array())
+ {
+ $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
+ $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment',
+ 'rows' => 4, 'cols' => 55, 'title' => $this->cal->gettext('itipcommenttitle')));
+
+ return html::div($attrib,
+ html::div(null, $input->show() . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) .
+ html::p('attendees-commentbox', html::label(null, $this->cal->gettext('itipcomment') . $textarea->show()))
+ );
+ }
+
+ /**
+ *
+ */
+ function resources_form($attrib = array())
+ {
+ $input = new html_inputfield(array('name' => 'resource', 'id' => 'edit-resource-name', 'size' => 30));
+
+ return html::div($attrib,
+ html::div(null, $input->show() . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-add', 'value' => $this->cal->gettext('addresource'))) . " " .
+ html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-resource-find', 'value' => $this->cal->gettext('findresources').'...')))
+ );
+ }
+
+ /**
+ *
+ */
+ function resources_list($attrib = array())
+ {
+ $attrib += array('id' => 'calendar-resources-list');
+
+ $this->rc->output->add_gui_object('resourceslist', $attrib['id']);
+
+ return html::tag('ul', $attrib, '', html::$common_attrib);
+ }
+
+ /**
+ *
+ */
+ public function resource_info($attrib = array())
+ {
+ $attrib += array('id' => 'calendar-resources-info');
+
+ $this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
+ $this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
+
+ // copy address book labels for owner details to client
+ $this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
+
+ $table_attrib = array('id','class','style','width','summary','cellpadding','cellspacing','border');
+
+ return html::tag('table', $attrib,
+ html::tag('tbody', null, ''), $table_attrib) .
+
+ html::tag('table', array('id' => $attrib['id'] . '-owner', 'style' => 'display:none') + $attrib,
+ html::tag('thead', null,
+ html::tag('tr', null,
+ html::tag('td', array('colspan' => 2), Q($this->cal->gettext('resourceowner')))
+ )
+ ) .
+ html::tag('tbody', null, ''),
+ $table_attrib);
+ }
+
+ /**
+ *
+ */
+ public function resource_calendar($attrib = array())
+ {
+ $attrib += array('id' => 'calendar-resources-calendar');
+
+ $this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
+
+ return html::div($attrib, '');
+ }
+
+ /**
+ * GUI object 'searchform' for the resource finder dialog
+ *
+ * @param array Named parameters
+ * @return string HTML code for the gui object
+ */
+ function resources_search_form($attrib)
+ {
+ $attrib += array('command' => 'search-resource', 'id' => 'rcmcalresqsearchbox', 'autocomplete' => 'off');
+ $attrib['name'] = '_q';
+
+ $input_q = new html_inputfield($attrib);
+ $out = $input_q->show();
+
+ // add form tag around text field
+ $out = $this->rc->output->form_tag(array(
+ 'name' => "rcmcalresoursqsearchform",
+ 'onsubmit' => rcmail_output::JS_OBJECT_NAME . ".command('" . $attrib['command'] . "'); return false",
+ 'style' => "display:inline"),
+ $out);
+
+ return $out;
+ }
+
+ /**
+ *
+ */
+ function attendees_freebusy_table($attrib = array())
+ {
+ $table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
+ $table->add('attendees',
+ html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) .
+ html::div('timesheader', '&nbsp;') .
+ html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
+ );
+ $table->add('times',
+ html::div('scroll',
+ html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) .
+ html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), '&nbsp;')
+ )
+ );
+
+ return $table->show($attrib);
+ }
+
+ /**
+ *
+ */
+ function event_invitebox($attrib = array())
+ {
+ if ($this->cal->event) {
+ return html::div($attrib,
+ $this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation')) .
+ $this->cal->invitestatus
+ );
+ }
+
+ return '';
+ }
+
+ function event_rsvp_buttons($attrib = array())
+ {
+ $actions = array('accepted','tentative','declined');
+ if ($attrib['delegate'] !== 'false')
+ $actions[] = 'delegated';
+
+ return $this->cal->itip->itip_rsvp_buttons($attrib, $actions);
+ }
+
+}
diff --git a/calendar/lib/encryption.php b/calendar/lib/encryption.php
new file mode 100644
index 0000000..c503109
--- /dev/null
+++ b/calendar/lib/encryption.php
@@ -0,0 +1,166 @@
+<?php
+/**
+ * A class to handle secure encryption and decryption of arbitrary data
+ * Copyright http://stackoverflow.com/users/338665/ircmaxell
+ * http://stackoverflow.com/questions/5089841/php-2-way-encryption-i-need-to-store-passwords-that-can-be-retrieved
+ *
+ *
+ * Note that this is not just straight encryption. It also has a few other
+ * features in it to make the encrypted data far more secure. Note that any
+ * other implementations used to decrypt data will have to do the same exact
+ * operations.
+ *
+ * Security Benefits:
+ *
+ * - Uses Key stretching
+ * - Hides the Initialization Vector
+ * - Does HMAC verification of source data
+ *
+ */
+class Encryption {
+
+ /**
+ * @var string $cipher The mcrypt cipher to use for this instance
+ */
+ protected $cipher = '';
+
+ /**
+ * @var int $mode The mcrypt cipher mode to use
+ */
+ protected $mode = '';
+
+ /**
+ * @var int $rounds The number of rounds to feed into PBKDF2 for key generation
+ */
+ protected $rounds = 100;
+
+ /**
+ * Constructor!
+ *
+ * @param string $cipher The MCRYPT_* cypher to use for this instance
+ * @param int $mode The MCRYPT_MODE_* mode to use for this instance
+ * @param int $rounds The number of PBKDF2 rounds to do on the key
+ */
+ public function __construct($cipher, $mode, $rounds = 100) {
+ $this->cipher = $cipher;
+ $this->mode = $mode;
+ $this->rounds = (int) $rounds;
+ }
+
+ /**
+ * Decrypt the data with the provided key
+ *
+ * @param string $data The encrypted datat to decrypt
+ * @param string $key The key to use for decryption
+ *
+ * @returns string|false The returned string if decryption is successful
+ * false if it is not
+ */
+ public function decrypt($data, $key) {
+ $salt = substr($data, 0, 128);
+ $enc = substr($data, 128, -64);
+ $mac = substr($data, -64);
+
+ list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
+
+ if ($mac !== hash_hmac('sha512', $enc, $macKey, true)) {
+ return false;
+ }
+
+ $dec = mcrypt_decrypt($this->cipher, $cipherKey, $enc, $this->mode, $iv);
+
+ $data = $this->unpad($dec);
+
+ return $data;
+ }
+
+ /**
+ * Encrypt the supplied data using the supplied key
+ *
+ * @param string $data The data to encrypt
+ * @param string $key The key to encrypt with
+ *
+ * @returns string The encrypted data
+ */
+ public function encrypt($data, $key) {
+ $salt = mcrypt_create_iv(128, MCRYPT_DEV_URANDOM);
+ list ($cipherKey, $macKey, $iv) = $this->getKeys($salt, $key);
+
+ $data = $this->pad($data);
+
+ $enc = mcrypt_encrypt($this->cipher, $cipherKey, $data, $this->mode, $iv);
+
+ $mac = hash_hmac('sha512', $enc, $macKey, true);
+ return $salt . $enc . $mac;
+ }
+
+ /**
+ * Generates a set of keys given a random salt and a master key
+ *
+ * @param string $salt A random string to change the keys each encryption
+ * @param string $key The supplied key to encrypt with
+ *
+ * @returns array An array of keys (a cipher key, a mac key, and a IV)
+ */
+ protected function getKeys($salt, $key) {
+ $ivSize = mcrypt_get_iv_size($this->cipher, $this->mode);
+ $keySize = mcrypt_get_key_size($this->cipher, $this->mode);
+ $length = 2 * $keySize + $ivSize;
+
+ $key = $this->pbkdf2('sha512', $key, $salt, $this->rounds, $length);
+
+ $cipherKey = substr($key, 0, $keySize);
+ $macKey = substr($key, $keySize, $keySize);
+ $iv = substr($key, 2 * $keySize);
+ return array($cipherKey, $macKey, $iv);
+ }
+
+ /**
+ * Stretch the key using the PBKDF2 algorithm
+ *
+ * @see http://en.wikipedia.org/wiki/PBKDF2
+ *
+ * @param string $algo The algorithm to use
+ * @param string $key The key to stretch
+ * @param string $salt A random salt
+ * @param int $rounds The number of rounds to derive
+ * @param int $length The length of the output key
+ *
+ * @returns string The derived key.
+ */
+ protected function pbkdf2($algo, $key, $salt, $rounds, $length) {
+ $size = strlen(hash($algo, '', true));
+ $len = ceil($length / $size);
+ $result = '';
+ for ($i = 1; $i <= $len; $i++) {
+ $tmp = hash_hmac($algo, $salt . pack('N', $i), $key, true);
+ $res = $tmp;
+ for ($j = 1; $j < $rounds; $j++) {
+ $tmp = hash_hmac($algo, $tmp, $key, true);
+ $res ^= $tmp;
+ }
+ $result .= $res;
+ }
+ return substr($result, 0, $length);
+ }
+
+ protected function pad($data) {
+ $length = mcrypt_get_block_size($this->cipher, $this->mode);
+ $padAmount = $length - strlen($data) % $length;
+ if ($padAmount == 0) {
+ $padAmount = $length;
+ }
+ return $data . str_repeat(chr($padAmount), $padAmount);
+ }
+
+ protected function unpad($data) {
+ $length = mcrypt_get_block_size($this->cipher, $this->mode);
+ $last = ord($data[strlen($data) - 1]);
+ if ($last > $length) return false;
+ if (substr($data, -1 * $last) !== str_repeat(chr($last), $last)) {
+ return false;
+ }
+ return substr($data, 0, -1 * $last);
+ }
+}
+?>
diff --git a/calendar/lib/js/fullcalendar.js b/calendar/lib/js/fullcalendar.js
new file mode 100644
index 0000000..3b79107
--- /dev/null
+++ b/calendar/lib/js/fullcalendar.js
@@ -0,0 +1,6845 @@
+/*!
+ * FullCalendar v1.6.4-rcube-1.1.3
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw, 2014 Kolab Systems AG
+ */
+
+/*
+ * Use fullcalendar.css for basic styling.
+ * For event drag & drop, requires jQuery UI draggable.
+ * For event resizing, requires jQuery UI resizable.
+ */
+
+(function($, undefined) {
+
+
+;;
+
+var defaults = {
+
+ // display
+ defaultView: 'month',
+ aspectRatio: 1.35,
+ header: {
+ left: 'title',
+ center: '',
+ right: 'today prev,next'
+ },
+ weekends: true,
+ weekNumbers: false,
+ weekNumberCalculation: 'iso',
+ weekNumberTitle: 'W',
+ currentTimeIndicator: false,
+
+ // editing
+ //editable: false,
+ //disableDragging: false,
+ //disableResizing: false,
+
+ allDayDefault: true,
+ ignoreTimezone: true,
+
+ // event ajax
+ lazyFetching: true,
+ startParam: 'start',
+ endParam: 'end',
+
+ // time formats
+ titleFormat: {
+ month: 'MMMM yyyy',
+ week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
+ day: 'dddd, MMM d, yyyy',
+ list: 'MMM d, yyyy',
+ table: 'MMM d, yyyy'
+ },
+ columnFormat: {
+ month: 'ddd',
+ week: 'ddd M/d',
+ day: 'dddd M/d',
+ list: 'dddd, MMM d, yyyy',
+ table: 'MMM d, yyyy'
+ },
+ timeFormat: { // for event elements
+ '': 'h(:mm)t' // default
+ },
+
+ // locale
+ isRTL: false,
+ firstDay: 0,
+ monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
+ monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
+ dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
+ dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
+ buttonText: {
+ prev: "<span class='fc-text-arrow'>&lsaquo;</span>",
+ next: "<span class='fc-text-arrow'>&rsaquo;</span>",
+ prevYear: "<span class='fc-text-arrow'>&laquo;</span>",
+ nextYear: "<span class='fc-text-arrow'>&raquo;</span>",
+ today: 'today',
+ month: 'month',
+ week: 'week',
+ day: 'day',
+ list: 'list',
+ table: 'table'
+ },
+ listTexts: {
+ until: 'until',
+ past: 'Past events',
+ today: 'Today',
+ tomorrow: 'Tomorrow',
+ thisWeek: 'This week',
+ nextWeek: 'Next week',
+ thisMonth: 'This month',
+ nextMonth: 'Next month',
+ future: 'Future events',
+ week: 'W'
+ },
+
+ // list/table options
+ listSections: 'month', // false|'day'|'week'|'month'|'smart'
+ listRange: 30, // number of days to be displayed
+ listPage: 7, // number of days to jump when paging
+ tableCols: ['handle', 'date', 'time', 'title'],
+
+ // jquery-ui theming
+ theme: false,
+ buttonIcons: {
+ prev: 'circle-triangle-w',
+ next: 'circle-triangle-e'
+ },
+
+ //selectable: false,
+ unselectAuto: true,
+
+ dropAccept: '*',
+
+ handleWindowResize: true
+
+};
+
+// right-to-left defaults
+var rtlDefaults = {
+ header: {
+ left: 'next,prev today',
+ center: '',
+ right: 'title'
+ },
+ buttonText: {
+ prev: "<span class='fc-text-arrow'>&rsaquo;</span>",
+ next: "<span class='fc-text-arrow'>&lsaquo;</span>",
+ prevYear: "<span class='fc-text-arrow'>&raquo;</span>",
+ nextYear: "<span class='fc-text-arrow'>&laquo;</span>"
+ },
+ buttonIcons: {
+ prev: 'circle-triangle-e',
+ next: 'circle-triangle-w'
+ }
+};
+
+
+
+;;
+
+var fc = $.fullCalendar = { version: "1.6.4-rcube-1.1.3" };
+var fcViews = fc.views = {};
+
+
+$.fn.fullCalendar = function(options) {
+
+
+ // method calling
+ if (typeof options == 'string') {
+ var args = Array.prototype.slice.call(arguments, 1);
+ var res;
+ this.each(function() {
+ var calendar = $.data(this, 'fullCalendar');
+ if (calendar && $.isFunction(calendar[options])) {
+ var r = calendar[options].apply(calendar, args);
+ if (res === undefined) {
+ res = r;
+ }
+ if (options == 'destroy') {
+ $.removeData(this, 'fullCalendar');
+ }
+ }
+ });
+ if (res !== undefined) {
+ return res;
+ }
+ return this;
+ }
+
+ options = options || {};
+
+ // would like to have this logic in EventManager, but needs to happen before options are recursively extended
+ var eventSources = options.eventSources || [];
+ delete options.eventSources;
+ if (options.events) {
+ eventSources.push(options.events);
+ delete options.events;
+ }
+
+
+ options = $.extend(true, {},
+ defaults,
+ (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
+ options
+ );
+
+
+ this.each(function(i, _element) {
+ var element = $(_element);
+ var calendar = new Calendar(element, options, eventSources);
+ element.data('fullCalendar', calendar); // TODO: look into memory leak implications
+ calendar.render();
+ });
+
+
+ return this;
+
+};
+
+
+// function for adding/overriding defaults
+function setDefaults(d) {
+ $.extend(true, defaults, d);
+}
+
+
+
+;;
+
+
+function Calendar(element, options, eventSources) {
+ var t = this;
+
+
+ // exports
+ t.options = options;
+ t.render = render;
+ t.destroy = destroy;
+ t.refetchEvents = refetchEvents;
+ t.reportEvents = reportEvents;
+ t.reportEventChange = reportEventChange;
+ t.rerenderEvents = rerenderEvents;
+ t.changeView = changeView;
+ t.select = select;
+ t.unselect = unselect;
+ t.prev = prev;
+ t.next = next;
+ t.prevYear = prevYear;
+ t.nextYear = nextYear;
+ t.today = today;
+ t.gotoDate = gotoDate;
+ t.incrementDate = incrementDate;
+ t.formatDate = function(format, date) { return formatDate(format, date, options) };
+ t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
+ t.getDate = getDate;
+ t.getView = getView;
+ t.option = option;
+ t.trigger = trigger;
+
+
+ // imports
+ EventManager.call(t, options, eventSources);
+ var isFetchNeeded = t.isFetchNeeded;
+ var fetchEvents = t.fetchEvents;
+
+
+ // locals
+ var _element = element[0];
+ var header;
+ var headerElement;
+ var content;
+ var tm; // for making theme classes
+ var currentView;
+ var elementOuterWidth;
+ var suggestedViewHeight;
+ var resizeUID = 0;
+ var ignoreWindowResize = 0;
+ var lazyRendering = false;
+ var date = new Date();
+ var events = [];
+ var _dragElement;
+
+
+
+ /* Main Rendering
+ -----------------------------------------------------------------------------*/
+
+
+ setYMD(date, options.year, options.month, options.date);
+
+
+ function render(inc) {
+ if (!content) {
+ initialRender();
+ }
+ else if (elementVisible()) {
+ // mainly for the public API
+ calcSize();
+ _renderView(inc);
+ }
+ }
+
+
+ function initialRender() {
+ tm = options.theme ? 'ui' : 'fc';
+ element.addClass('fc');
+ if (options.isRTL) {
+ element.addClass('fc-rtl');
+ }
+ else {
+ element.addClass('fc-ltr');
+ }
+ if (options.theme) {
+ element.addClass('ui-widget');
+ }
+
+ content = $("<div class='fc-content' style='position:relative'/>")
+ .prependTo(element);
+
+ header = new Header(t, options);
+ headerElement = header.render();
+ if (headerElement) {
+ element.prepend(headerElement);
+ }
+
+ changeView(options.defaultView);
+
+ if (options.handleWindowResize) {
+ $(window).resize(windowResize);
+ }
+
+ // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
+ if (!bodyVisible()) {
+ lateRender();
+ }
+ }
+
+
+ // called when we know the calendar couldn't be rendered when it was initialized,
+ // but we think it's ready now
+ function lateRender() {
+ setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
+ if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
+ renderView();
+ }
+ },0);
+ }
+
+
+ function destroy() {
+
+ if (currentView) {
+ trigger('viewDestroy', currentView, currentView, currentView.element);
+ currentView.triggerEventDestroy();
+ }
+
+ $(window).unbind('resize', windowResize);
+
+ header.destroy();
+ content.remove();
+ element.removeClass('fc fc-rtl ui-widget');
+ }
+
+
+ function elementVisible() {
+ return element.is(':visible');
+ }
+
+
+ function bodyVisible() {
+ return $('body').is(':visible');
+ }
+
+
+
+ /* View Rendering
+ -----------------------------------------------------------------------------*/
+
+
+ function changeView(newViewName) {
+ if (!currentView || newViewName != currentView.name) {
+ _changeView(newViewName);
+ }
+ }
+
+
+ function _changeView(newViewName) {
+ ignoreWindowResize++;
+
+ if (currentView) {
+ trigger('viewDestroy', currentView, currentView, currentView.element);
+ unselect();
+ currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
+ freezeContentHeight();
+ currentView.element.remove();
+ header.deactivateButton(currentView.name);
+ }
+
+ header.activateButton(newViewName);
+
+ currentView = new fcViews[newViewName](
+ $("<div class='fc-view fc-view-" + newViewName + "' style='position:relative'/>")
+ .appendTo(content),
+ t // the calendar object
+ );
+
+ renderView();
+ unfreezeContentHeight();
+
+ ignoreWindowResize--;
+ }
+
+
+ function renderView(inc) {
+ if (
+ !currentView.start || // never rendered before
+ inc || date < currentView.start || date >= currentView.end // or new date range
+ ) {
+ if (elementVisible()) {
+ _renderView(inc);
+ }
+ }
+ }
+
+
+ function _renderView(inc) { // assumes elementVisible
+ ignoreWindowResize++;
+
+ if (currentView.start) { // already been rendered?
+ trigger('viewDestroy', currentView, currentView, currentView.element);
+ unselect();
+ clearEvents();
+ }
+
+ freezeContentHeight();
+ currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else
+ setSize();
+ unfreezeContentHeight();
+ (currentView.afterRender || noop)();
+
+ updateTitle();
+ updateTodayButton();
+
+ trigger('viewRender', currentView, currentView, currentView.element);
+ currentView.trigger('viewDisplay', _element); // deprecated
+
+ ignoreWindowResize--;
+
+ getAndRenderEvents();
+ }
+
+
+
+ /* Resizing
+ -----------------------------------------------------------------------------*/
+
+
+ function updateSize() {
+ if (elementVisible()) {
+ unselect();
+ clearEvents();
+ calcSize();
+ setSize();
+ unselect();
+ currentView.clearEvents();
+ currentView.trigger('viewRender', currentView);
+ currentView.renderEvents(events);
+ currentView.sizeDirty = false;
+ }
+ }
+
+
+ function calcSize() { // assumes elementVisible
+ if (options.contentHeight) {
+ suggestedViewHeight = options.contentHeight;
+ }
+ else if (options.height) {
+ suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
+ }
+ else {
+ suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
+ }
+ }
+
+
+ function setSize() { // assumes elementVisible
+
+ if (suggestedViewHeight === undefined) {
+ calcSize(); // for first time
+ // NOTE: we don't want to recalculate on every renderView because
+ // it could result in oscillating heights due to scrollbars.
+ }
+
+ ignoreWindowResize++;
+ currentView.setHeight(suggestedViewHeight);
+ currentView.setWidth(content.width());
+ ignoreWindowResize--;
+
+ elementOuterWidth = element.outerWidth();
+ }
+
+
+ function windowResize() {
+ if (!ignoreWindowResize) {
+ if (currentView.start) { // view has already been rendered
+ var uid = ++resizeUID;
+ setTimeout(function() { // add a delay
+ if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
+ if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
+ ignoreWindowResize++; // in case the windowResize callback changes the height
+ updateSize();
+ currentView.trigger('windowResize', _element);
+ ignoreWindowResize--;
+ }
+ }
+ }, 200);
+ }else{
+ // calendar must have been initialized in a 0x0 iframe that has just been resized
+ lateRender();
+ }
+ }
+ }
+
+
+
+ /* Event Fetching/Rendering
+ -----------------------------------------------------------------------------*/
+ // TODO: going forward, most of this stuff should be directly handled by the view
+
+
+ function refetchEvents(source, lazy) { // can be called as an API method
+ lazyRendering = lazy || false;
+ if (!lazyRendering) {
+ clearEvents();
+ }
+ fetchAndRenderEvents(source);
+ }
+
+
+ function rerenderEvents(modifiedEventID) { // can be called as an API method
+ clearEvents();
+ renderEvents(modifiedEventID);
+ }
+
+
+ function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack
+ if (elementVisible()) {
+ currentView.setEventData(events); // for View.js, TODO: unify with renderEvents
+ currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements
+ currentView.trigger('eventAfterAllRender');
+ }
+ }
+
+
+ function clearEvents() {
+ currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
+ currentView.clearEvents(); // actually remove the DOM elements
+ currentView.clearEventData(); // for View.js, TODO: unify with clearEvents
+ }
+
+
+ function getAndRenderEvents() {
+ if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
+ fetchAndRenderEvents();
+ }
+ else {
+ renderEvents();
+ }
+ }
+
+
+ function fetchAndRenderEvents(source) {
+ fetchEvents(currentView.visStart, currentView.visEnd, source);
+ // ... will call reportEvents
+ // ... which will call renderEvents
+ }
+
+
+ // called when event data arrives
+ function reportEvents(_events) {
+ if (lazyRendering) {
+ clearEvents();
+ lazyRendering = false;
+ }
+ events = _events;
+ renderEvents();
+ }
+
+
+ // called when a single event's data has been changed
+ function reportEventChange(eventID) {
+ rerenderEvents(eventID);
+ }
+
+
+
+ /* Header Updating
+ -----------------------------------------------------------------------------*/
+
+
+ function updateTitle() {
+ header.updateTitle(currentView.title);
+ }
+
+
+ function updateTodayButton() {
+ var today = new Date();
+ if (today >= currentView.start && today < currentView.end) {
+ header.disableButton('today');
+ }
+ else {
+ header.enableButton('today');
+ }
+ }
+
+
+
+ /* Selection
+ -----------------------------------------------------------------------------*/
+
+
+ function select(start, end, allDay) {
+ currentView.select(start, end, allDay===undefined ? true : allDay);
+ }
+
+
+ function unselect() { // safe to be called before renderView
+ if (currentView) {
+ currentView.unselect();
+ }
+ }
+
+
+
+ /* Date
+ -----------------------------------------------------------------------------*/
+
+
+ function prev() {
+ renderView(-1);
+ }
+
+
+ function next() {
+ renderView(1);
+ }
+
+
+ function prevYear() {
+ addYears(date, -1);
+ renderView();
+ }
+
+
+ function nextYear() {
+ addYears(date, 1);
+ renderView();
+ }
+
+
+ function today() {
+ date = new Date();
+ renderView();
+ }
+
+
+ function gotoDate(year, month, dateOfMonth) {
+ if (year instanceof Date) {
+ date = cloneDate(year); // provided 1 argument, a Date
+ }else{
+ setYMD(date, year, month, dateOfMonth);
+ }
+ renderView();
+ }
+
+
+ function incrementDate(years, months, days) {
+ if (years !== undefined) {
+ addYears(date, years);
+ }
+ if (months !== undefined) {
+ addMonths(date, months);
+ }
+ if (days !== undefined) {
+ addDays(date, days);
+ }
+ renderView();
+ }
+
+
+ function getDate() {
+ return cloneDate(date);
+ }
+
+
+
+ /* Height "Freezing"
+ -----------------------------------------------------------------------------*/
+
+
+ function freezeContentHeight() {
+ content.css({
+ width: '100%',
+ height: content.height(),
+ overflow: 'hidden'
+ });
+ }
+
+
+ function unfreezeContentHeight() {
+ content.css({
+ width: '',
+ height: '',
+ overflow: ''
+ });
+ }
+
+
+
+ /* Misc
+ -----------------------------------------------------------------------------*/
+
+
+ function getView() {
+ return currentView;
+ }
+
+
+ function option(name, value) {
+ if (value === undefined) {
+ return options[name];
+ }
+ if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
+ options[name] = value;
+ updateSize();
+ } else if (name.indexOf('list') == 0 || name == 'tableCols') {
+ options[name] = value;
+ currentView.start = null; // force re-render
+ } else if (name == 'maxHeight') {
+ options[name] = value;
+ }
+ }
+
+
+ function trigger(name, thisObj) {
+ if (options[name]) {
+ return options[name].apply(
+ thisObj || _element,
+ Array.prototype.slice.call(arguments, 2)
+ );
+ }
+ }
+
+
+
+ /* External Dragging
+ ------------------------------------------------------------------------*/
+
+ if (options.droppable) {
+ $(document)
+ .bind('dragstart', function(ev, ui) {
+ var _e = ev.target;
+ var e = $(_e);
+ if (!e.parents('.fc').length) { // not already inside a calendar
+ var accept = options.dropAccept;
+ if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
+ _dragElement = _e;
+ currentView.dragStart(_dragElement, ev, ui);
+ }
+ }
+ })
+ .bind('dragstop', function(ev, ui) {
+ if (_dragElement) {
+ currentView.dragStop(_dragElement, ev, ui);
+ _dragElement = null;
+ }
+ });
+ }
+
+
+}
+
+;;
+
+function Header(calendar, options) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+ t.destroy = destroy;
+ t.updateTitle = updateTitle;
+ t.activateButton = activateButton;
+ t.deactivateButton = deactivateButton;
+ t.disableButton = disableButton;
+ t.enableButton = enableButton;
+
+
+ // locals
+ var element = $([]);
+ var tm;
+
+
+
+ function render() {
+ tm = options.theme ? 'ui' : 'fc';
+ var sections = options.header;
+ if (sections) {
+ element = $("<table class='fc-header' style='width:100%'/>")
+ .append(
+ $("<tr/>")
+ .append(renderSection('left'))
+ .append(renderSection('center'))
+ .append(renderSection('right'))
+ );
+ return element;
+ }
+ }
+
+
+ function destroy() {
+ element.remove();
+ }
+
+
+ function renderSection(position) {
+ var e = $("<td class='fc-header-" + position + "'/>");
+ var buttonStr = options.header[position];
+ if (buttonStr) {
+ $.each(buttonStr.split(' '), function(i) {
+ if (i > 0) {
+ e.append("<span class='fc-header-space'/>");
+ }
+ var prevButton;
+ $.each(this.split(','), function(j, buttonName) {
+ if (buttonName == 'title') {
+ e.append("<span class='fc-header-title'><h2 aria-live='polite' aria-relevant='text' aria-atomic='true'>&nbsp;</h2></span>");
+ if (prevButton) {
+ prevButton.addClass(tm + '-corner-right');
+ }
+ prevButton = null;
+ }else{
+ var buttonClick;
+ if (calendar[buttonName]) {
+ buttonClick = calendar[buttonName]; // calendar method
+ }
+ else if (fcViews[buttonName]) {
+ buttonClick = function() {
+ button.removeClass(tm + '-state-hover'); // forget why
+ calendar.changeView(buttonName);
+ };
+ }
+ if (buttonClick) {
+ var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
+ var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
+ var button = $(
+ "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default' role='button' tabindex='0'>" +
+ (icon ?
+ "<span class='fc-icon-wrap'>" +
+ "<span class='ui-icon ui-icon-" + icon + "'/>" +
+ "</span>" :
+ text
+ ) +
+ "</span>"
+ )
+ .click(function() {
+ if (!button.hasClass(tm + '-state-disabled')) {
+ buttonClick();
+ }
+ })
+ .mousedown(function() {
+ button
+ .not('.' + tm + '-state-active')
+ .not('.' + tm + '-state-disabled')
+ .addClass(tm + '-state-down');
+ })
+ .mouseup(function() {
+ button.removeClass(tm + '-state-down');
+ })
+ .hover(
+ function() {
+ button
+ .not('.' + tm + '-state-active')
+ .not('.' + tm + '-state-disabled')
+ .addClass(tm + '-state-hover');
+ },
+ function() {
+ button
+ .removeClass(tm + '-state-hover')
+ .removeClass(tm + '-state-down');
+ }
+ )
+ .keypress(function(ev) {
+ if (ev.keyCode == 13)
+ $(ev.target).trigger('click');
+ })
+ .appendTo(e);
+ disableTextSelection(button);
+ if (!prevButton) {
+ button.addClass(tm + '-corner-left');
+ }
+ prevButton = button;
+ }
+ }
+ });
+ if (prevButton) {
+ prevButton.addClass(tm + '-corner-right');
+ }
+ });
+ }
+ return e;
+ }
+
+
+ function updateTitle(html) {
+ element.find('h2')
+ .html(html);
+ }
+
+
+ function activateButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .addClass(tm + '-state-active').attr('tabindex', '-1');
+ }
+
+
+ function deactivateButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .removeClass(tm + '-state-active').attr('tabindex', '0');
+ }
+
+
+ function disableButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .addClass(tm + '-state-disabled').attr('tabindex', '-1');
+ }
+
+
+ function enableButton(buttonName) {
+ element.find('span.fc-button-' + buttonName)
+ .removeClass(tm + '-state-disabled').attr('tabindex', '0');
+ }
+
+
+}
+
+;;
+
+fc.sourceNormalizers = [];
+fc.sourceFetchers = [];
+
+var ajaxDefaults = {
+ dataType: 'json',
+ cache: false
+};
+
+var eventGUID = 1;
+
+
+function EventManager(options, _sources) {
+ var t = this;
+
+
+ // exports
+ t.isFetchNeeded = isFetchNeeded;
+ t.fetchEvents = fetchEvents;
+ t.addEventSource = addEventSource;
+ t.removeEventSource = removeEventSource;
+ t.removeEventSources = removeEventSources;
+ t.updateEvent = updateEvent;
+ t.renderEvent = renderEvent;
+ t.removeEvents = removeEvents;
+ t.clientEvents = clientEvents;
+ t.normalizeEvent = normalizeEvent;
+
+
+ // imports
+ var trigger = t.trigger;
+ var getView = t.getView;
+ var reportEvents = t.reportEvents;
+
+
+ // locals
+ var stickySource = { events: [] };
+ var sources = [ stickySource ];
+ var rangeStart, rangeEnd;
+ var currentFetchID = 0;
+ var pendingSourceCnt = 0;
+ var loadingLevel = 0;
+ var cache = [];
+
+
+ for (var i=0; i<_sources.length; i++) {
+ _addEventSource(_sources[i]);
+ }
+
+
+
+ /* Fetching
+ -----------------------------------------------------------------------------*/
+
+
+ function isFetchNeeded(start, end) {
+ return !rangeStart || start < rangeStart || end > rangeEnd;
+ }
+
+
+ function fetchEvents(start, end, src) {
+ rangeStart = start;
+ rangeEnd = end;
+ // partially clear cache if refreshing one source only (issue #1061)
+ cache = typeof src != 'undefined' ? $.grep(cache, function(e) { return !isSourcesEqual(e.source, src); }) : [];
+ var fetchID = ++currentFetchID;
+ var len = sources.length;
+ pendingSourceCnt = typeof src == 'undefined' ? len : 1;
+ for (var i=0; i<len; i++) {
+ if (typeof src == 'undefined' || isSourcesEqual(sources[i], src))
+ fetchEventSource(sources[i], fetchID);
+ }
+ }
+
+
+
+ function fetchEventSource(source, fetchID) {
+ _fetchEventSource(source, function(events) {
+ if (fetchID == currentFetchID) {
+ if (events) {
+
+ if (options.eventDataTransform) {
+ events = $.map(events, options.eventDataTransform);
+ }
+ if (source.eventDataTransform) {
+ events = $.map(events, source.eventDataTransform);
+ }
+ // TODO: this technique is not ideal for static array event sources.
+ // For arrays, we'll want to process all events right in the beginning, then never again.
+
+ for (var i=0; i<events.length; i++) {
+ events[i].source = source;
+ normalizeEvent(events[i]);
+ }
+ cache = cache.concat(events);
+ }
+ pendingSourceCnt--;
+ if (!pendingSourceCnt) {
+ reportEvents(cache);
+ }
+ }
+ });
+ }
+
+
+ function _fetchEventSource(source, callback) {
+ var i;
+ var fetchers = fc.sourceFetchers;
+ var res;
+ for (i=0; i<fetchers.length; i++) {
+ res = fetchers[i](source, rangeStart, rangeEnd, callback);
+ if (res === true) {
+ // the fetcher is in charge. made its own async request
+ return;
+ }
+ else if (typeof res == 'object') {
+ // the fetcher returned a new source. process it
+ _fetchEventSource(res, callback);
+ return;
+ }
+ }
+ var events = source.events;
+ if (events) {
+ if ($.isFunction(events)) {
+ pushLoading();
+ events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
+ callback(events);
+ popLoading();
+ });
+ }
+ else if ($.isArray(events)) {
+ callback(events);
+ }
+ else {
+ callback();
+ }
+ }else{
+ var url = source.url;
+ if (url) {
+ var success = source.success;
+ var error = source.error;
+ var complete = source.complete;
+
+ // retrieve any outbound GET/POST $.ajax data from the options
+ var customData;
+ if ($.isFunction(source.data)) {
+ // supplied as a function that returns a key/value object
+ customData = source.data();
+ }
+ else {
+ // supplied as a straight key/value object
+ customData = source.data;
+ }
+
+ // use a copy of the custom data so we can modify the parameters
+ // and not affect the passed-in object.
+ var data = $.extend({}, customData || {});
+
+ var startParam = firstDefined(source.startParam, options.startParam);
+ var endParam = firstDefined(source.endParam, options.endParam);
+ if (startParam) {
+ data[startParam] = Math.round(+rangeStart / 1000);
+ }
+ if (endParam) {
+ data[endParam] = Math.round(+rangeEnd / 1000);
+ }
+
+ pushLoading();
+ $.ajax($.extend({}, ajaxDefaults, source, {
+ data: data,
+ success: function(events) {
+ events = events || [];
+ var res = applyAll(success, this, arguments);
+ if ($.isArray(res)) {
+ events = res;
+ }
+ callback(events);
+ },
+ error: function() {
+ applyAll(error, this, arguments);
+ callback();
+ },
+ complete: function() {
+ applyAll(complete, this, arguments);
+ popLoading();
+ }
+ }));
+ }else{
+ callback();
+ }
+ }
+ }
+
+
+
+ /* Sources
+ -----------------------------------------------------------------------------*/
+
+
+ function addEventSource(source) {
+ source = _addEventSource(source);
+ if (source) {
+ pendingSourceCnt++;
+ fetchEventSource(source, currentFetchID); // will eventually call reportEvents
+ }
+ }
+
+
+ function _addEventSource(source) {
+ if ($.isFunction(source) || $.isArray(source)) {
+ source = { events: source };
+ }
+ else if (typeof source == 'string') {
+ source = { url: source };
+ }
+ if (typeof source == 'object') {
+ normalizeSource(source);
+ sources.push(source);
+ return source;
+ }
+ }
+
+
+ function removeEventSource(source) {
+ sources = $.grep(sources, function(src) {
+ return !isSourcesEqual(src, source);
+ });
+ // remove all client events from that source
+ cache = $.grep(cache, function(e) {
+ return !isSourcesEqual(e.source, source);
+ });
+ reportEvents(cache);
+ }
+
+
+ function removeEventSources() {
+ sources = [];
+ removeEvents();
+ }
+
+
+
+ /* Manipulation
+ -----------------------------------------------------------------------------*/
+
+
+ function updateEvent(event) { // update an existing event
+ var i, len = cache.length, e,
+ defaultEventEnd = getView().defaultEventEnd, // getView???
+ startDelta = event.start - event._start,
+ endDelta = event.end ?
+ (event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end
+ : 0; // was null and event was just resized
+ for (i=0; i<len; i++) {
+ e = cache[i];
+ if (e._id == event._id && e != event) {
+ e.start = new Date(+e.start + startDelta);
+ if (event.end) {
+ if (e.end) {
+ e.end = new Date(+e.end + endDelta);
+ }else{
+ e.end = new Date(+defaultEventEnd(e) + endDelta);
+ }
+ }else{
+ e.end = null;
+ }
+ e.title = event.title;
+ e.url = event.url;
+ e.allDay = event.allDay;
+ e.className = event.className;
+ e.editable = event.editable;
+ e.color = event.color;
+ e.backgroundColor = event.backgroundColor;
+ e.borderColor = event.borderColor;
+ e.textColor = event.textColor;
+ normalizeEvent(e);
+ }
+ }
+ normalizeEvent(event);
+ reportEvents(cache);
+ }
+
+
+ function renderEvent(event, stick) {
+ normalizeEvent(event);
+ if (!event.source) {
+ if (stick) {
+ stickySource.events.push(event);
+ event.source = stickySource;
+ }
+ }
+ // always push event to cache (issue #1112:)
+ cache.push(event);
+ reportEvents(cache);
+ }
+
+
+ function removeEvents(filter) {
+ if (!filter) { // remove all
+ cache = [];
+ // clear all array sources
+ for (var i=0; i<sources.length; i++) {
+ if ($.isArray(sources[i].events)) {
+ sources[i].events = [];
+ }
+ }
+ }else{
+ if (!$.isFunction(filter)) { // an event ID
+ var id = filter + '';
+ filter = function(e) {
+ return e._id == id;
+ };
+ }
+ cache = $.grep(cache, filter, true);
+ // remove events from array sources
+ for (var i=0; i<sources.length; i++) {
+ if ($.isArray(sources[i].events)) {
+ sources[i].events = $.grep(sources[i].events, filter, true);
+ }
+ }
+ }
+ reportEvents(cache);
+ }
+
+
+ function clientEvents(filter) {
+ if ($.isFunction(filter)) {
+ return $.grep(cache, filter);
+ }
+ else if (filter) { // an event ID
+ filter += '';
+ return $.grep(cache, function(e) {
+ return e._id == filter;
+ });
+ }
+ return cache; // else, return all
+ }
+
+
+
+ /* Loading State
+ -----------------------------------------------------------------------------*/
+
+
+ function pushLoading() {
+ if (!loadingLevel++) {
+ trigger('loading', null, true, getView());
+ }
+ }
+
+
+ function popLoading() {
+ if (!--loadingLevel) {
+ trigger('loading', null, false, getView());
+ }
+ }
+
+
+
+ /* Event Normalization
+ -----------------------------------------------------------------------------*/
+
+
+ function normalizeEvent(event) {
+ var source = event.source || {};
+ var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
+ event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
+ if (event.date) {
+ if (!event.start) {
+ event.start = event.date;
+ }
+ delete event.date;
+ }
+ event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
+ event.end = parseDate(event.end, ignoreTimezone);
+ if (event.end && event.end <= event.start) {
+ event.end = null;
+ }
+ event._end = event.end ? cloneDate(event.end) : null;
+ if (event.allDay === undefined) {
+ event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
+ }
+ if (event.className) {
+ if (typeof event.className == 'string') {
+ event.className = event.className.split(/\s+/);
+ }
+ }else{
+ event.className = [];
+ }
+ // TODO: if there is no start date, return false to indicate an invalid event
+ }
+
+
+
+ /* Utils
+ ------------------------------------------------------------------------------*/
+
+
+ function normalizeSource(source) {
+ if (source.className) {
+ // TODO: repeat code, same code for event classNames
+ if (typeof source.className == 'string') {
+ source.className = source.className.split(/\s+/);
+ }
+ }else{
+ source.className = [];
+ }
+ var normalizers = fc.sourceNormalizers;
+ for (var i=0; i<normalizers.length; i++) {
+ normalizers[i](source);
+ }
+ }
+
+
+ function isSourcesEqual(source1, source2) {
+ return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
+ }
+
+
+ function getSourcePrimitive(source) {
+ return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
+ }
+
+
+}
+
+;;
+
+
+fc.addDays = addDays;
+fc.cloneDate = cloneDate;
+fc.parseDate = parseDate;
+fc.parseISO8601 = parseISO8601;
+fc.parseTime = parseTime;
+fc.formatDate = formatDate;
+fc.formatDates = formatDates;
+
+
+
+/* Date Math
+-----------------------------------------------------------------------------*/
+
+var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
+ DAY_MS = 86400000,
+ HOUR_MS = 3600000,
+ MINUTE_MS = 60000;
+
+
+function addYears(d, n, keepTime) {
+ d.setFullYear(d.getFullYear() + n);
+ if (!keepTime) {
+ clearTime(d);
+ }
+ return d;
+}
+
+
+function addMonths(d, n, keepTime) { // prevents day overflow/underflow
+ if (+d) { // prevent infinite looping on invalid dates
+ var m = d.getMonth() + n,
+ check = cloneDate(d);
+ check.setDate(1);
+ check.setMonth(m);
+ d.setMonth(m);
+ if (!keepTime) {
+ clearTime(d);
+ }
+ while (d.getMonth() != check.getMonth()) {
+ d.setDate(d.getDate() + (d < check ? 1 : -1));
+ }
+ }
+ return d;
+}
+
+
+function addDays(d, n, keepTime) { // deals with daylight savings
+ if (+d) {
+ var dd = d.getDate() + n,
+ check = cloneDate(d);
+ check.setHours(9); // set to middle of day
+ check.setDate(dd);
+ d.setDate(dd);
+ if (!keepTime) {
+ clearTime(d);
+ }
+ fixDate(d, check);
+ }
+ return d;
+}
+
+
+function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
+ if (+d) { // prevent infinite looping on invalid dates
+ while (d.getDate() != check.getDate()) {
+ d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
+ }
+ }
+}
+
+
+function addMinutes(d, n) {
+ d.setMinutes(d.getMinutes() + n);
+ return d;
+}
+
+
+function clearTime(d) {
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ d.setMilliseconds(0);
+ return d;
+}
+
+
+function cloneDate(d, dontKeepTime) {
+ if (dontKeepTime) {
+ return clearTime(new Date(+d));
+ }
+ return new Date(+d);
+}
+
+
+function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
+ var i=0, d;
+ do {
+ d = new Date(1970, i++, 1);
+ } while (d.getHours()); // != 0
+ return d;
+}
+
+
+function dayDiff(d1, d2) { // d1 - d2
+ return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
+}
+
+
+function setYMD(date, y, m, d) {
+ if (y !== undefined && y != date.getFullYear()) {
+ date.setDate(1);
+ date.setMonth(0);
+ date.setFullYear(y);
+ }
+ if (m !== undefined && m != date.getMonth()) {
+ date.setDate(1);
+ date.setMonth(m);
+ }
+ if (d !== undefined) {
+ date.setDate(d);
+ }
+}
+
+
+
+/* Date Parsing
+-----------------------------------------------------------------------------*/
+
+
+function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
+ if (typeof s == 'object') { // already a Date object
+ return s;
+ }
+ if (typeof s == 'number') { // a UNIX timestamp
+ return new Date(s * 1000);
+ }
+ if (typeof s == 'string') {
+ if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp
+ return new Date(parseFloat(s) * 1000);
+ }
+ if (ignoreTimezone === undefined) {
+ ignoreTimezone = true;
+ }
+ return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
+ }
+ // TODO: never return invalid dates (like from new Date(<string>)), return null instead
+ return null;
+}
+
+
+function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
+ // derived from http://delete.me.uk/2005/03/iso8601.html
+ // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
+ var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
+ if (!m) {
+ return null;
+ }
+ var date = new Date(m[1], 0, 2);
+ if (ignoreTimezone || !m[13]) {
+ var check = new Date(m[1], 0, 2, 9, 0);
+ if (m[3]) {
+ date.setMonth(m[3] - 1);
+ check.setMonth(m[3] - 1);
+ }
+ if (m[5]) {
+ date.setDate(m[5]);
+ check.setDate(m[5]);
+ }
+ fixDate(date, check);
+ if (m[7]) {
+ date.setHours(m[7]);
+ }
+ if (m[8]) {
+ date.setMinutes(m[8]);
+ }
+ if (m[10]) {
+ date.setSeconds(m[10]);
+ }
+ if (m[12]) {
+ date.setMilliseconds(Number("0." + m[12]) * 1000);
+ }
+ fixDate(date, check);
+ }else{
+ date.setUTCFullYear(
+ m[1],
+ m[3] ? m[3] - 1 : 0,
+ m[5] || 1
+ );
+ date.setUTCHours(
+ m[7] || 0,
+ m[8] || 0,
+ m[10] || 0,
+ m[12] ? Number("0." + m[12]) * 1000 : 0
+ );
+ if (m[14]) {
+ var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
+ offset *= m[15] == '-' ? 1 : -1;
+ date = new Date(+date + (offset * 60 * 1000));
+ }
+ }
+ return date;
+}
+
+
+function parseTime(s) { // returns minutes since start of day
+ if (typeof s == 'number') { // an hour
+ return s * 60;
+ }
+ if (typeof s == 'object') { // a Date object
+ return s.getHours() * 60 + s.getMinutes();
+ }
+ var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
+ if (m) {
+ var h = parseInt(m[1], 10);
+ if (m[3]) {
+ h %= 12;
+ if (m[3].toLowerCase().charAt(0) == 'p') {
+ h += 12;
+ }
+ }
+ return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
+ }
+}
+
+
+
+/* Date Formatting
+-----------------------------------------------------------------------------*/
+// TODO: use same function formatDate(date, [date2], format, [options])
+
+
+function formatDate(date, format, options) {
+ return formatDates(date, null, format, options);
+}
+
+
+function formatDates(date1, date2, format, options) {
+ options = options || defaults;
+ var date = date1,
+ otherDate = date2,
+ i, len = format.length, c,
+ i2, formatter,
+ res = '';
+ for (i=0; i<len; i++) {
+ c = format.charAt(i);
+ if (c == "'") {
+ for (i2=i+1; i2<len; i2++) {
+ if (format.charAt(i2) == "'") {
+ if (date) {
+ if (i2 == i+1) {
+ res += "'";
+ }else{
+ res += format.substring(i+1, i2);
+ }
+ i = i2;
+ }
+ break;
+ }
+ }
+ }
+ else if (c == '(') {
+ for (i2=i+1; i2<len; i2++) {
+ if (format.charAt(i2) == ')') {
+ var subres = formatDate(date, format.substring(i+1, i2), options);
+ if (parseInt(subres.replace(/\D/, ''), 10)) {
+ res += subres;
+ }
+ i = i2;
+ break;
+ }
+ }
+ }
+ else if (c == '[') {
+ for (i2=i+1; i2<len; i2++) {
+ if (format.charAt(i2) == ']') {
+ var subformat = format.substring(i+1, i2);
+ var subres = formatDate(date, subformat, options);
+ if (subres != formatDate(otherDate, subformat, options)) {
+ res += subres;
+ }
+ i = i2;
+ break;
+ }
+ }
+ }
+ else if (c == '{') {
+ date = date2;
+ otherDate = date1;
+ }
+ else if (c == '}') {
+ date = date1;
+ otherDate = date2;
+ }
+ else {
+ for (i2=len; i2>i; i2--) {
+ if (formatter = dateFormatters[format.substring(i, i2)]) {
+ if (date) {
+ res += formatter(date, options);
+ }
+ i = i2 - 1;
+ break;
+ }
+ }
+ if (i2 == i) {
+ if (date) {
+ res += c;
+ }
+ }
+ }
+ }
+ return res;
+};
+
+
+var dateFormatters = {
+ s : function(d) { return d.getSeconds() },
+ ss : function(d) { return zeroPad(d.getSeconds()) },
+ m : function(d) { return d.getMinutes() },
+ mm : function(d) { return zeroPad(d.getMinutes()) },
+ h : function(d) { return d.getHours() % 12 || 12 },
+ hh : function(d) { return zeroPad(d.getHours() % 12 || 12) },
+ H : function(d) { return d.getHours() },
+ HH : function(d) { return zeroPad(d.getHours()) },
+ d : function(d) { return d.getDate() },
+ dd : function(d) { return zeroPad(d.getDate()) },
+ ddd : function(d,o) { return o.dayNamesShort[d.getDay()] },
+ dddd: function(d,o) { return o.dayNames[d.getDay()] },
+ M : function(d) { return d.getMonth() + 1 },
+ MM : function(d) { return zeroPad(d.getMonth() + 1) },
+ MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] },
+ MMMM: function(d,o) { return o.monthNames[d.getMonth()] },
+ yy : function(d) { return (d.getFullYear()+'').substring(2) },
+ yyyy: function(d) { return d.getFullYear() },
+ t : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
+ tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
+ T : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
+ TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' },
+ u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
+ S : function(d) {
+ var date = d.getDate();
+ if (date > 10 && date < 20) {
+ return 'th';
+ }
+ return ['st', 'nd', 'rd'][date%10-1] || 'th';
+ },
+ w : function(d, o) { // local
+ return o.weekNumberCalculation(d);
+ },
+ W : function(d) { // ISO
+ return iso8601Week(d);
+ }
+};
+fc.dateFormatters = dateFormatters;
+
+
+/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js)
+ *
+ * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
+ * `date` - the date to get the week for
+ * `number` - the number of the week within the year that contains this date
+ */
+function iso8601Week(date) {
+ var time;
+ var checkDate = new Date(date.getTime());
+
+ // Find Thursday of this week starting on Monday
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
+
+ time = checkDate.getTime();
+ checkDate.setMonth(0); // Compare with Jan 1
+ checkDate.setDate(1);
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+}
+
+// Determine the week of the year based on the ISO 8601 definition.
+// copied from jquery UI Datepicker
+var iso8601Week = function(date) {
+ var checkDate = cloneDate(date);
+ // Find Thursday of this week starting on Monday
+ checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
+ var time = checkDate.getTime();
+ checkDate.setMonth(0); // Compare with Jan 1
+ checkDate.setDate(1);
+ return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
+};
+
+
+;;
+
+fc.applyAll = applyAll;
+
+
+/* Event Date Math
+-----------------------------------------------------------------------------*/
+
+
+function exclEndDay(event) {
+ if (event.end) {
+ return _exclEndDay(event.end, event.allDay);
+ }else{
+ return addDays(cloneDate(event.start), 1);
+ }
+}
+
+
+function _exclEndDay(end, allDay) {
+ end = cloneDate(end);
+ return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
+ // why don't we check for seconds/ms too?
+}
+
+
+
+/* Event Element Binding
+-----------------------------------------------------------------------------*/
+
+
+function lazySegBind(container, segs, bindHandlers) {
+ container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
+ var parent=ev.target, e,
+ i, seg;
+ while (parent != this) {
+ e = parent;
+ parent = parent.parentNode;
+ }
+ if ((i = e._fci) !== undefined) {
+ e._fci = undefined;
+ seg = segs[i];
+ bindHandlers(seg.event, seg.element, seg);
+ $(ev.target).trigger(ev);
+ }
+ ev.stopPropagation();
+ });
+}
+
+
+
+/* Element Dimensions
+-----------------------------------------------------------------------------*/
+
+
+function setOuterWidth(element, width, includeMargins) {
+ for (var i=0, e; i<element.length; i++) {
+ e = $(element[i]);
+ e.width(Math.max(0, width - hsides(e, includeMargins)));
+ }
+}
+
+
+function setOuterHeight(element, height, includeMargins) {
+ for (var i=0, e; i<element.length; i++) {
+ e = $(element[i]);
+ e.height(Math.max(0, height - vsides(e, includeMargins)));
+ }
+}
+
+
+function hsides(element, includeMargins) {
+ return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
+}
+
+
+function hpadding(element) {
+ return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
+ (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
+}
+
+
+function hmargins(element) {
+ return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
+ (parseFloat($.css(element[0], 'marginRight', true)) || 0);
+}
+
+
+function hborders(element) {
+ return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
+ (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
+}
+
+
+function vsides(element, includeMargins) {
+ return vpadding(element) + vborders(element) + (includeMargins ? vmargins(element) : 0);
+}
+
+
+function vpadding(element) {
+ return (parseFloat($.css(element[0], 'paddingTop', true)) || 0) +
+ (parseFloat($.css(element[0], 'paddingBottom', true)) || 0);
+}
+
+
+function vmargins(element) {
+ return (parseFloat($.css(element[0], 'marginTop', true)) || 0) +
+ (parseFloat($.css(element[0], 'marginBottom', true)) || 0);
+}
+
+
+function vborders(element) {
+ return (parseFloat($.css(element[0], 'borderTopWidth', true)) || 0) +
+ (parseFloat($.css(element[0], 'borderBottomWidth', true)) || 0);
+}
+
+
+
+/* Misc Utils
+-----------------------------------------------------------------------------*/
+
+
+//TODO: arraySlice
+//TODO: isFunction, grep ?
+
+
+function noop() { }
+
+
+function dateCompare(a, b) {
+ return a - b;
+}
+
+
+function arrayMax(a) {
+ return Math.max.apply(Math, a);
+}
+
+
+function zeroPad(n) {
+ return (n < 10 ? '0' : '') + n;
+}
+
+
+function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
+ if (obj[name] !== undefined) {
+ return obj[name];
+ }
+ var parts = name.split(/(?=[A-Z])/),
+ i=parts.length-1, res;
+ for (; i>=0; i--) {
+ res = obj[parts[i].toLowerCase()];
+ if (res !== undefined) {
+ return res;
+ }
+ }
+ return obj[''];
+}
+
+
+function htmlEscape(s) {
+ return s.replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/'/g, '&#039;')
+ .replace(/"/g, '&quot;')
+ .replace(/\n/g, '<br />');
+}
+
+
+function disableTextSelection(element) {
+ element
+ .attr('unselectable', 'on')
+ .css('MozUserSelect', 'none')
+ .bind('selectstart.ui', function() { return false; });
+}
+
+
+/*
+function enableTextSelection(element) {
+ element
+ .attr('unselectable', 'off')
+ .css('MozUserSelect', '')
+ .unbind('selectstart.ui');
+}
+*/
+
+
+function markFirstLast(e) {
+ e.children()
+ .removeClass('fc-first fc-last')
+ .filter(':first-child')
+ .addClass('fc-first')
+ .end()
+ .filter(':last-child')
+ .addClass('fc-last');
+}
+
+
+function setDayID(cell, date) {
+ cell.each(function(i, _cell) {
+ _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
+ // TODO: make a way that doesn't rely on order of classes
+ });
+}
+
+
+function getSkinCss(event, opt) {
+ var source = event.source || {};
+ var eventColor = event.color;
+ var sourceColor = source.color;
+ var optionColor = opt('eventColor');
+ var backgroundColor =
+ event.backgroundColor ||
+ eventColor ||
+ source.backgroundColor ||
+ sourceColor ||
+ opt('eventBackgroundColor') ||
+ optionColor;
+ var borderColor =
+ event.borderColor ||
+ eventColor ||
+ source.borderColor ||
+ sourceColor ||
+ opt('eventBorderColor') ||
+ optionColor;
+ var textColor =
+ event.textColor ||
+ source.textColor ||
+ opt('eventTextColor');
+ var statements = [];
+ if (backgroundColor) {
+ statements.push('background-color:' + backgroundColor);
+ }
+ if (borderColor) {
+ statements.push('border-color:' + borderColor);
+ }
+ if (textColor) {
+ statements.push('color:' + textColor);
+ }
+ return statements.join(';');
+}
+
+
+function applyAll(functions, thisObj, args) {
+ if ($.isFunction(functions)) {
+ functions = [ functions ];
+ }
+ if (functions) {
+ var i;
+ var ret;
+ for (i=0; i<functions.length; i++) {
+ ret = functions[i].apply(thisObj, args) || ret;
+ }
+ return ret;
+ }
+}
+
+
+function firstDefined() {
+ for (var i=0; i<arguments.length; i++) {
+ if (arguments[i] !== undefined) {
+ return arguments[i];
+ }
+ }
+}
+
+
+;;
+
+fcViews.month = MonthView;
+
+function MonthView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ BasicView.call(t, element, calendar, 'month');
+ var opt = t.opt;
+ var renderBasic = t.renderBasic;
+ var skipHiddenDays = t.skipHiddenDays;
+ var getCellsPerWeek = t.getCellsPerWeek;
+ var formatDate = calendar.formatDate;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addMonths(date, delta);
+ date.setDate(1);
+ }
+
+ var firstDay = opt('firstDay');
+
+ var start = cloneDate(date, true);
+ start.setDate(1);
+
+ var end = addMonths(cloneDate(start), 1);
+
+ var visStart = cloneDate(start);
+ addDays(visStart, -((visStart.getDay() - firstDay + 7) % 7));
+ skipHiddenDays(visStart);
+
+ var visEnd = cloneDate(end);
+ addDays(visEnd, (7 - visEnd.getDay() + firstDay) % 7);
+ skipHiddenDays(visEnd, -1, true);
+
+ var colCnt = getCellsPerWeek();
+ var rowCnt = Math.round(dayDiff(visEnd, visStart) / 7); // should be no need for Math.round
+
+ if (opt('weekMode') == 'fixed') {
+ addDays(visEnd, (6 - rowCnt) * 7); // add weeks to make up for it
+ rowCnt = 6;
+ }
+
+ t.title = formatDate(start, opt('titleFormat'));
+
+ t.start = start;
+ t.end = end;
+ t.visStart = visStart;
+ t.visEnd = visEnd;
+
+ renderBasic(rowCnt, colCnt, true);
+ }
+
+
+}
+
+;;
+
+fcViews.basicWeek = BasicWeekView;
+
+function BasicWeekView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ BasicView.call(t, element, calendar, 'basicWeek');
+ var opt = t.opt;
+ var renderBasic = t.renderBasic;
+ var skipHiddenDays = t.skipHiddenDays;
+ var getCellsPerWeek = t.getCellsPerWeek;
+ var formatDates = calendar.formatDates;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta * 7);
+ }
+
+ var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
+ var end = addDays(cloneDate(start), 7);
+
+ var visStart = cloneDate(start);
+ skipHiddenDays(visStart);
+
+ var visEnd = cloneDate(end);
+ skipHiddenDays(visEnd, -1, true);
+
+ var colCnt = getCellsPerWeek();
+
+ t.start = start;
+ t.end = end;
+ t.visStart = visStart;
+ t.visEnd = visEnd;
+
+ t.title = formatDates(
+ visStart,
+ addDays(cloneDate(visEnd), -1),
+ opt('titleFormat')
+ );
+
+ renderBasic(1, colCnt, false);
+ }
+
+
+}
+
+;;
+
+fcViews.basicDay = BasicDayView;
+
+
+function BasicDayView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ BasicView.call(t, element, calendar, 'basicDay');
+ var opt = t.opt;
+ var renderBasic = t.renderBasic;
+ var skipHiddenDays = t.skipHiddenDays;
+ var formatDate = calendar.formatDate;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta);
+ }
+ skipHiddenDays(date, delta < 0 ? -1 : 1);
+
+ var start = cloneDate(date, true);
+ var end = addDays(cloneDate(start), 1);
+
+ t.title = formatDate(date, opt('titleFormat'));
+
+ t.start = t.visStart = start;
+ t.end = t.visEnd = end;
+
+ renderBasic(1, 1, false);
+ }
+
+
+}
+
+;;
+
+setDefaults({
+ weekMode: 'fixed'
+});
+
+
+function BasicView(element, calendar, viewName) {
+ var t = this;
+
+
+ // exports
+ t.renderBasic = renderBasic;
+ t.setHeight = setHeight;
+ t.setWidth = setWidth;
+ t.renderDayOverlay = renderDayOverlay;
+ t.defaultSelectionEnd = defaultSelectionEnd;
+ t.renderSelection = renderSelection;
+ t.clearSelection = clearSelection;
+ t.reportDayClick = reportDayClick; // for selection (kinda hacky)
+ t.dragStart = dragStart;
+ t.dragStop = dragStop;
+ t.defaultEventEnd = defaultEventEnd;
+ t.getHoverListener = function() { return hoverListener };
+ t.colLeft = colLeft;
+ t.colRight = colRight;
+ t.colContentLeft = colContentLeft;
+ t.colContentRight = colContentRight;
+ t.getIsCellAllDay = function() { return true };
+ t.allDayRow = allDayRow;
+ t.getRowCnt = function() { return rowCnt };
+ t.getColCnt = function() { return colCnt };
+ t.getColWidth = function() { return colWidth };
+ t.getDaySegmentContainer = function() { return daySegmentContainer };
+
+
+ // imports
+ View.call(t, element, calendar, viewName);
+ OverlayManager.call(t);
+ SelectionManager.call(t);
+ BasicEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var renderOverlay = t.renderOverlay;
+ var clearOverlays = t.clearOverlays;
+ var daySelectionMousedown = t.daySelectionMousedown;
+ var cellToDate = t.cellToDate;
+ var dateToCell = t.dateToCell;
+ var rangeToSegments = t.rangeToSegments;
+ var formatDate = calendar.formatDate;
+
+
+ // locals
+
+ var table;
+ var head;
+ var headCells;
+ var body;
+ var bodyRows;
+ var bodyCells;
+ var bodyFirstCells;
+ var firstRowCellInners;
+ var firstRowCellContentInners;
+ var daySegmentContainer;
+
+ var viewWidth;
+ var viewHeight;
+ var colWidth;
+ var weekNumberWidth;
+
+ var rowCnt, colCnt;
+ var showNumbers;
+ var coordinateGrid;
+ var hoverListener;
+ var colPositions;
+ var colContentPositions;
+
+ var tm;
+ var colFormat;
+ var showWeekNumbers;
+ var weekNumberTitle;
+ var weekNumberFormat;
+
+
+
+ /* Rendering
+ ------------------------------------------------------------*/
+
+
+ disableTextSelection(element.addClass('fc-grid'));
+
+
+ function renderBasic(_rowCnt, _colCnt, _showNumbers) {
+ rowCnt = _rowCnt;
+ colCnt = _colCnt;
+ showNumbers = _showNumbers;
+ updateOptions();
+
+ if (!body) {
+ buildEventContainer();
+ }
+
+ buildTable();
+ }
+
+
+ function updateOptions() {
+ tm = opt('theme') ? 'ui' : 'fc';
+ colFormat = opt('columnFormat');
+
+ // week # options. (TODO: bad, logic also in other views)
+ showWeekNumbers = opt('weekNumbers');
+ weekNumberTitle = opt('weekNumberTitle');
+ if (opt('weekNumberCalculation') != 'iso') {
+ weekNumberFormat = "w";
+ }
+ else {
+ weekNumberFormat = "W";
+ }
+ }
+
+
+ function buildEventContainer() {
+ daySegmentContainer =
+ $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
+ .appendTo(element);
+ }
+
+
+ function buildTable() {
+ var html = buildTableHTML();
+
+ if (table) {
+ table.remove();
+ }
+ table = $(html).appendTo(element);
+
+ head = table.find('thead');
+ headCells = head.find('.fc-day-header');
+ body = table.find('tbody');
+ bodyRows = body.find('tr');
+ bodyCells = body.find('.fc-day');
+ bodyFirstCells = bodyRows.find('td:first-child');
+
+ firstRowCellInners = bodyRows.eq(0).find('.fc-day > div');
+ firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div');
+
+ markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
+ markFirstLast(bodyRows); // marks first+last td's
+ bodyRows.eq(0).addClass('fc-first');
+ bodyRows.filter(':last').addClass('fc-last');
+
+ bodyCells.each(function(i, _cell) {
+ var date = cellToDate(
+ Math.floor(i / colCnt),
+ i % colCnt
+ );
+ trigger('dayRender', t, date, $(_cell));
+ });
+
+ dayBind(bodyCells);
+ }
+
+
+
+ /* HTML Building
+ -----------------------------------------------------------*/
+
+
+ function buildTableHTML() {
+ var html =
+ "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
+ buildHeadHTML() +
+ buildBodyHTML() +
+ "</table>";
+
+ return html;
+ }
+
+
+ function buildHeadHTML() {
+ var headerClass = tm + "-widget-header";
+ var html = '';
+ var col;
+ var date;
+
+ html += "<thead><tr>";
+
+ if (showWeekNumbers) {
+ html +=
+ "<th class='fc-week-number " + headerClass + "'>" +
+ htmlEscape(weekNumberTitle) +
+ "</th>";
+ }
+
+ for (col=0; col<colCnt; col++) {
+ date = cellToDate(0, col);
+ html +=
+ "<th class='fc-day-header fc-" + dayIDs[date.getDay()] + " " + headerClass + "'>" +
+ htmlEscape(formatDate(date, colFormat)) +
+ "</th>";
+ }
+
+ html += "</tr></thead>";
+
+ return html;
+ }
+
+
+ function buildBodyHTML() {
+ var contentClass = tm + "-widget-content";
+ var html = '';
+ var row;
+ var col;
+ var date;
+
+ html += "<tbody>";
+
+ for (row=0; row<rowCnt; row++) {
+
+ html += "<tr class='fc-week'>";
+
+ if (showWeekNumbers) {
+ date = cellToDate(row, 0);
+ html +=
+ "<td class='fc-week-number " + contentClass + "'>" +
+ "<div>" +
+ htmlEscape(formatDate(date, weekNumberFormat)) +
+ "</div>" +
+ "</td>";
+ }
+
+ for (col=0; col<colCnt; col++) {
+ date = cellToDate(row, col);
+ html += buildCellHTML(date);
+ }
+
+ html += "</tr>";
+ }
+
+ html += "</tbody>";
+
+ return html;
+ }
+
+
+ function buildCellHTML(date) {
+ var contentClass = tm + "-widget-content";
+ var month = t.start.getMonth();
+ var today = clearTime(new Date());
+ var html = '';
+ var classNames = [
+ 'fc-day',
+ 'fc-' + dayIDs[date.getDay()],
+ contentClass
+ ];
+
+ if (date.getMonth() != month) {
+ classNames.push('fc-other-month');
+ }
+ if (+date == +today) {
+ classNames.push(
+ 'fc-today',
+ tm + '-state-highlight'
+ );
+ }
+ else if (date < today) {
+ classNames.push('fc-past');
+ }
+ else {
+ classNames.push('fc-future');
+ }
+
+ html +=
+ "<td" +
+ " class='" + classNames.join(' ') + "'" +
+ " data-date='" + formatDate(date, 'yyyy-MM-dd') + "'" +
+ ">" +
+ "<div>";
+
+ if (showNumbers) {
+ html += "<div class='fc-day-number'>" + date.getDate() + "</div>";
+ }
+
+ html +=
+ "<div class='fc-day-content'>" +
+ "<div style='position:relative'>&nbsp;</div>" +
+ "</div>" +
+ "</div>" +
+ "</td>";
+
+ return html;
+ }
+
+
+
+ /* Dimensions
+ -----------------------------------------------------------*/
+
+
+ function setHeight(height) {
+ viewHeight = height;
+
+ var bodyHeight = viewHeight - head.height();
+ var rowHeight;
+ var rowHeightLast;
+ var cell;
+
+ if (opt('weekMode') == 'variable') {
+ rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
+ }else{
+ rowHeight = Math.floor(bodyHeight / rowCnt);
+ rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
+ }
+
+ bodyFirstCells.each(function(i, _cell) {
+ if (i < rowCnt) {
+ cell = $(_cell);
+ cell.find('> div').css(
+ 'min-height',
+ (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
+ );
+ }
+ });
+
+ }
+
+
+ function setWidth(width) {
+ viewWidth = width;
+ colPositions.clear();
+ colContentPositions.clear();
+
+ weekNumberWidth = 0;
+ if (showWeekNumbers) {
+ weekNumberWidth = head.find('th.fc-week-number').outerWidth();
+ }
+
+ colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt);
+ setOuterWidth(headCells.slice(0, -1), colWidth);
+ }
+
+
+
+ /* Day clicking and binding
+ -----------------------------------------------------------*/
+
+
+ function dayBind(days) {
+ days.click(dayClick)
+ .mousedown(daySelectionMousedown);
+ }
+
+
+ function dayClick(ev) {
+ if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
+ var date = parseISO8601($(this).data('date'));
+ trigger('dayClick', this, date, true, ev);
+ }
+ }
+
+
+
+ /* Semi-transparent Overlay Helpers
+ ------------------------------------------------------*/
+ // TODO: should be consolidated with AgendaView's methods
+
+
+ function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
+
+ if (refreshCoordinateGrid) {
+ coordinateGrid.build();
+ }
+
+ var segments = rangeToSegments(overlayStart, overlayEnd);
+
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ dayBind(
+ renderCellOverlay(
+ segment.row,
+ segment.leftCol,
+ segment.row,
+ segment.rightCol
+ )
+ );
+ }
+ }
+
+
+ function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
+ var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
+ return renderOverlay(rect, element);
+ }
+
+
+
+ /* Selection
+ -----------------------------------------------------------------------*/
+
+
+ function defaultSelectionEnd(startDate, allDay) {
+ return cloneDate(startDate);
+ }
+
+
+ function renderSelection(startDate, endDate, allDay) {
+ renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
+ }
+
+
+ function clearSelection() {
+ clearOverlays();
+ }
+
+
+ function reportDayClick(date, allDay, ev) {
+ var cell = dateToCell(date);
+ var _element = bodyCells[cell.row*colCnt + cell.col];
+ trigger('dayClick', _element, date, allDay, ev);
+ }
+
+
+
+ /* External Dragging
+ -----------------------------------------------------------------------*/
+
+
+ function dragStart(_dragElement, ev, ui) {
+ hoverListener.start(function(cell) {
+ clearOverlays();
+ if (cell) {
+ renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
+ }
+ }, ev);
+ }
+
+
+ function dragStop(_dragElement, ev, ui) {
+ var cell = hoverListener.stop();
+ clearOverlays();
+ if (cell) {
+ var d = cellToDate(cell);
+ trigger('drop', _dragElement, d, true, ev, ui);
+ }
+ }
+
+
+
+ /* Utilities
+ --------------------------------------------------------*/
+
+
+ function defaultEventEnd(event) {
+ return cloneDate(event.start);
+ }
+
+
+ coordinateGrid = new CoordinateGrid(function(rows, cols) {
+ var e, n, p;
+ headCells.each(function(i, _e) {
+ e = $(_e);
+ n = e.offset().left;
+ if (i) {
+ p[1] = n;
+ }
+ p = [n];
+ cols[i] = p;
+ });
+ p[1] = n + e.outerWidth();
+ bodyRows.each(function(i, _e) {
+ if (i < rowCnt) {
+ e = $(_e);
+ n = e.offset().top;
+ if (i) {
+ p[1] = n;
+ }
+ p = [n];
+ rows[i] = p;
+ }
+ });
+ p[1] = n + e.outerHeight();
+ });
+
+
+ hoverListener = new HoverListener(coordinateGrid);
+
+ colPositions = new HorizontalPositionCache(function(col) {
+ return firstRowCellInners.eq(col);
+ });
+
+ colContentPositions = new HorizontalPositionCache(function(col) {
+ return firstRowCellContentInners.eq(col);
+ });
+
+
+ function colLeft(col) {
+ return colPositions.left(col);
+ }
+
+
+ function colRight(col) {
+ return colPositions.right(col);
+ }
+
+
+ function colContentLeft(col) {
+ return colContentPositions.left(col);
+ }
+
+
+ function colContentRight(col) {
+ return colContentPositions.right(col);
+ }
+
+
+ function allDayRow(i) {
+ return bodyRows.eq(i);
+ }
+
+}
+
+;;
+
+function BasicEventRenderer() {
+ var t = this;
+
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.clearEvents = clearEvents;
+
+
+ // imports
+ DayEventRenderer.call(t);
+
+
+ function renderEvents(events, modifiedEventId) {
+ t.renderDayEvents(events, modifiedEventId);
+ }
+
+
+ function clearEvents() {
+ t.getDaySegmentContainer().empty();
+ }
+
+
+ // TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div
+
+}
+
+;;
+
+fcViews.agendaWeek = AgendaWeekView;
+
+function AgendaWeekView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ AgendaView.call(t, element, calendar, 'agendaWeek');
+ var opt = t.opt;
+ var renderAgenda = t.renderAgenda;
+ var skipHiddenDays = t.skipHiddenDays;
+ var getCellsPerWeek = t.getCellsPerWeek;
+ var formatDates = calendar.formatDates;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta * 7);
+ }
+
+ var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
+ var end = addDays(cloneDate(start), 7);
+
+ var visStart = cloneDate(start);
+ skipHiddenDays(visStart);
+
+ var visEnd = cloneDate(end);
+ skipHiddenDays(visEnd, -1, true);
+
+ var colCnt = getCellsPerWeek();
+
+ t.title = formatDates(
+ visStart,
+ addDays(cloneDate(visEnd), -1),
+ opt('titleFormat')
+ );
+
+ t.start = start;
+ t.end = end;
+ t.visStart = visStart;
+ t.visEnd = visEnd;
+
+ renderAgenda(colCnt);
+ }
+
+}
+
+;;
+
+fcViews.agendaDay = AgendaDayView;
+
+
+function AgendaDayView(element, calendar) {
+ var t = this;
+
+
+ // exports
+ t.render = render;
+
+
+ // imports
+ AgendaView.call(t, element, calendar, 'agendaDay');
+ var opt = t.opt;
+ var renderAgenda = t.renderAgenda;
+ var skipHiddenDays = t.skipHiddenDays;
+ var formatDate = calendar.formatDate;
+
+
+ function render(date, delta) {
+
+ if (delta) {
+ addDays(date, delta);
+ }
+ skipHiddenDays(date, delta < 0 ? -1 : 1);
+
+ var start = cloneDate(date, true);
+ var end = addDays(cloneDate(start), 1);
+
+ t.title = formatDate(date, opt('titleFormat'));
+
+ t.start = t.visStart = start;
+ t.end = t.visEnd = end;
+
+ renderAgenda(1);
+ }
+
+
+}
+
+;;
+
+setDefaults({
+ allDaySlot: true,
+ allDayText: 'all-day',
+ firstHour: 6,
+ slotMinutes: 30,
+ defaultEventMinutes: 120,
+ axisFormat: 'h(:mm)tt',
+ timeFormat: {
+ agenda: 'h:mm{ - h:mm}'
+ },
+ dragOpacity: {
+ agenda: .5
+ },
+ minTime: 0,
+ maxTime: 24,
+ slotEventOverlap: true
+});
+
+
+// TODO: make it work in quirks mode (event corners, all-day height)
+// TODO: test liquid width, especially in IE6
+
+
+function AgendaView(element, calendar, viewName) {
+ var t = this;
+
+
+ // exports
+ t.renderAgenda = renderAgenda;
+ t.setWidth = setWidth;
+ t.setHeight = setHeight;
+ t.afterRender = afterRender;
+ t.defaultEventEnd = defaultEventEnd;
+ t.timePosition = timePosition;
+ t.getIsCellAllDay = getIsCellAllDay;
+ t.allDayRow = getAllDayRow;
+ t.getCoordinateGrid = function() { return coordinateGrid }; // specifically for AgendaEventRenderer
+ t.getHoverListener = function() { return hoverListener };
+ t.colLeft = colLeft;
+ t.colRight = colRight;
+ t.colContentLeft = colContentLeft;
+ t.colContentRight = colContentRight;
+ t.getDaySegmentContainer = function() { return daySegmentContainer };
+ t.getSlotSegmentContainer = function() { return slotSegmentContainer };
+ t.getMinMinute = function() { return minMinute };
+ t.getMaxMinute = function() { return maxMinute };
+ t.getSlotContainer = function() { return slotContainer };
+ t.getRowCnt = function() { return 1 };
+ t.getColCnt = function() { return colCnt };
+ t.getColWidth = function() { return colWidth };
+ t.getSnapHeight = function() { return snapHeight };
+ t.getSnapMinutes = function() { return snapMinutes };
+ t.defaultSelectionEnd = defaultSelectionEnd;
+ t.renderDayOverlay = renderDayOverlay;
+ t.renderSelection = renderSelection;
+ t.clearSelection = clearSelection;
+ t.reportDayClick = reportDayClick; // selection mousedown hack
+ t.dragStart = dragStart;
+ t.dragStop = dragStop;
+
+
+ // imports
+ View.call(t, element, calendar, viewName);
+ OverlayManager.call(t);
+ SelectionManager.call(t);
+ AgendaEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var renderOverlay = t.renderOverlay;
+ var clearOverlays = t.clearOverlays;
+ var reportSelection = t.reportSelection;
+ var unselect = t.unselect;
+ var daySelectionMousedown = t.daySelectionMousedown;
+ var slotSegHtml = t.slotSegHtml;
+ var cellToDate = t.cellToDate;
+ var dateToCell = t.dateToCell;
+ var rangeToSegments = t.rangeToSegments;
+ var formatDate = calendar.formatDate;
+
+
+ // locals
+
+ var dayTable;
+ var dayHead;
+ var dayHeadCells;
+ var dayBody;
+ var dayBodyCells;
+ var dayBodyCellInners;
+ var dayBodyCellContentInners;
+ var dayBodyFirstCell;
+ var dayBodyFirstCellStretcher;
+ var slotLayer;
+ var daySegmentContainer;
+ var allDayTable;
+ var allDayRow;
+ var slotScroller;
+ var slotContainer;
+ var slotSegmentContainer;
+ var slotTable;
+ var selectionHelper;
+
+ var viewWidth;
+ var viewHeight;
+ var axisWidth;
+ var colWidth;
+ var gutterWidth;
+ var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
+
+ var snapMinutes;
+ var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4)
+ var snapHeight; // holds the pixel hight of a "selection" slot
+
+ var colCnt;
+ var slotCnt;
+ var coordinateGrid;
+ var hoverListener;
+ var colPositions;
+ var colContentPositions;
+ var slotTopCache = {};
+
+ var tm;
+ var rtl;
+ var minMinute, maxMinute;
+ var colFormat;
+ var showWeekNumbers;
+ var weekNumberTitle;
+ var weekNumberFormat;
+
+
+
+ /* Rendering
+ -----------------------------------------------------------------------------*/
+
+
+ disableTextSelection(element.addClass('fc-agenda'));
+
+
+ function renderAgenda(c) {
+ colCnt = c;
+ updateOptions();
+
+ if (!dayTable) { // first time rendering?
+ buildSkeleton(); // builds day table, slot area, events containers
+ }
+ else {
+ buildDayTable(); // rebuilds day table
+ }
+ }
+
+
+ function updateOptions() {
+
+ tm = opt('theme') ? 'ui' : 'fc';
+ rtl = opt('isRTL')
+ minMinute = parseTime(opt('minTime'));
+ maxMinute = parseTime(opt('maxTime'));
+ colFormat = opt('columnFormat');
+
+ // week # options. (TODO: bad, logic also in other views)
+ showWeekNumbers = opt('weekNumbers');
+ weekNumberTitle = opt('weekNumberTitle');
+ if (opt('weekNumberCalculation') != 'iso') {
+ weekNumberFormat = "w";
+ }
+ else {
+ weekNumberFormat = "W";
+ }
+
+ snapMinutes = opt('snapMinutes') || opt('slotMinutes');
+ }
+
+
+
+ /* Build DOM
+ -----------------------------------------------------------------------*/
+
+
+ function buildSkeleton() {
+ var headerClass = tm + "-widget-header";
+ var contentClass = tm + "-widget-content";
+ var s;
+ var d;
+ var i;
+ var maxd;
+ var minutes;
+ var slotNormal = opt('slotMinutes') % 15 == 0;
+
+ buildDayTable();
+
+ slotLayer =
+ $("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
+ .appendTo(element);
+
+ if (opt('allDaySlot')) {
+
+ daySegmentContainer =
+ $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
+ .appendTo(slotLayer);
+
+ s =
+ "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
+ "<tr>" +
+ "<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
+ "<td>" +
+ "<div class='fc-day-content'><div style='position:relative'/></div>" +
+ "</td>" +
+ "<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
+ "</tr>" +
+ "</table>";
+ allDayTable = $(s).appendTo(slotLayer);
+ allDayRow = allDayTable.find('tr');
+
+ dayBind(allDayRow.find('td'));
+
+ slotLayer.append(
+ "<div class='fc-agenda-divider " + headerClass + "'>" +
+ "<div class='fc-agenda-divider-inner'/>" +
+ "</div>"
+ );
+
+ }else{
+
+ daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
+
+ }
+
+ slotScroller =
+ $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
+ .appendTo(slotLayer);
+
+ slotContainer =
+ $("<div style='position:relative;width:100%;overflow:hidden'/>")
+ .appendTo(slotScroller);
+
+ slotSegmentContainer =
+ $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
+ .appendTo(slotContainer);
+
+ s =
+ "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
+ "<tbody>";
+ d = zeroDate();
+ maxd = addMinutes(cloneDate(d), maxMinute);
+ addMinutes(d, minMinute);
+ slotCnt = 0;
+ for (i=0; d < maxd; i++) {
+ minutes = d.getMinutes();
+ s +=
+ "<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
+ "<th class='fc-agenda-axis " + headerClass + "'>" +
+ ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
+ "</th>" +
+ "<td class='" + contentClass + "'>" +
+ "<div style='position:relative'>&nbsp;</div>" +
+ "</td>" +
+ "</tr>";
+ addMinutes(d, opt('slotMinutes'));
+ slotCnt++;
+ }
+ s +=
+ "</tbody>" +
+ "</table>";
+ slotTable = $(s).appendTo(slotContainer);
+
+ slotBind(slotTable.find('td'));
+ }
+
+
+
+ /* Build Day Table
+ -----------------------------------------------------------------------*/
+
+
+ function buildDayTable() {
+ var html = buildDayTableHTML();
+
+ if (dayTable) {
+ dayTable.remove();
+ }
+ dayTable = $(html).appendTo(element);
+
+ dayHead = dayTable.find('thead');
+ dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter
+ dayBody = dayTable.find('tbody');
+ dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter
+ dayBodyCellInners = dayBodyCells.find('> div');
+ dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div');
+
+ dayBodyFirstCell = dayBodyCells.eq(0);
+ dayBodyFirstCellStretcher = dayBodyCellInners.eq(0);
+
+ markFirstLast(dayHead.add(dayHead.find('tr')));
+ markFirstLast(dayBody.add(dayBody.find('tr')));
+
+ // TODO: now that we rebuild the cells every time, we should call dayRender
+ }
+
+
+ function buildDayTableHTML() {
+ var html =
+ "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
+ buildDayTableHeadHTML() +
+ buildDayTableBodyHTML() +
+ "</table>";
+
+ return html;
+ }
+
+
+ function buildDayTableHeadHTML() {
+ var headerClass = tm + "-widget-header";
+ var date;
+ var html = '';
+ var weekText;
+ var col;
+
+ html +=
+ "<thead>" +
+ "<tr>";
+
+ if (showWeekNumbers) {
+ date = cellToDate(0, 0);
+ weekText = formatDate(date, weekNumberFormat);
+ if (rtl) {
+ weekText += weekNumberTitle;
+ }
+ else {
+ weekText = weekNumberTitle + weekText;
+ }
+ html +=
+ "<th class='fc-agenda-axis fc-week-number " + headerClass + "'>" +
+ htmlEscape(weekText) +
+ "</th>";
+ }
+ else {
+ html += "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
+ }
+
+ for (col=0; col<colCnt; col++) {
+ date = cellToDate(0, col);
+ html +=
+ "<th class='fc-" + dayIDs[date.getDay()] + " fc-col" + col + ' ' + headerClass + "'>" +
+ htmlEscape(formatDate(date, colFormat)) +
+ "</th>";
+ }
+
+ html +=
+ "<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
+ "</tr>" +
+ "</thead>";
+
+ return html;
+ }
+
+
+ function buildDayTableBodyHTML() {
+ var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called
+ var contentClass = tm + "-widget-content";
+ var date;
+ var today = clearTime(new Date());
+ var col;
+ var cellsHTML;
+ var cellHTML;
+ var classNames;
+ var html = '';
+
+ html +=
+ "<tbody>" +
+ "<tr>" +
+ "<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
+
+ cellsHTML = '';
+
+ for (col=0; col<colCnt; col++) {
+
+ date = cellToDate(0, col);
+
+ classNames = [
+ 'fc-col' + col,
+ 'fc-' + dayIDs[date.getDay()],
+ contentClass
+ ];
+ if (+date == +today) {
+ classNames.push(
+ tm + '-state-highlight',
+ 'fc-today'
+ );
+ }
+ else if (date < today) {
+ classNames.push('fc-past');
+ }
+ else {
+ classNames.push('fc-future');
+ }
+
+ cellHTML =
+ "<td class='" + classNames.join(' ') + "'>" +
+ "<div>" +
+ "<div class='fc-day-content'>" +
+ "<div style='position:relative'>&nbsp;</div>" +
+ "</div>" +
+ "</div>" +
+ "</td>";
+
+ cellsHTML += cellHTML;
+ }
+
+ html += cellsHTML;
+ html +=
+ "<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
+ "</tr>" +
+ "</tbody>";
+
+ return html;
+ }
+
+
+ // TODO: data-date on the cells
+
+
+
+ /* Dimensions
+ -----------------------------------------------------------------------*/
+
+
+ function setHeight(height) {
+ if (height === undefined) {
+ height = viewHeight;
+ }
+ viewHeight = height;
+ slotTopCache = {};
+
+ var headHeight = dayBody.position().top;
+ var allDayHeight = slotScroller.position().top; // including divider
+ var bodyHeight = Math.min( // total body height, including borders
+ height - headHeight, // when scrollbars
+ slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
+ );
+
+ dayBodyFirstCellStretcher
+ .height(bodyHeight - vsides(dayBodyFirstCell));
+
+ slotLayer.css('top', headHeight);
+
+ slotScroller.height(bodyHeight - allDayHeight - 1);
+
+ // the stylesheet guarantees that the first row has no border.
+ // this allows .height() to work well cross-browser.
+ slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border
+
+ snapRatio = opt('slotMinutes') / snapMinutes;
+ snapHeight = slotHeight / snapRatio;
+ }
+
+
+ function setWidth(width) {
+ viewWidth = width;
+ colPositions.clear();
+ colContentPositions.clear();
+
+ var axisFirstCells = dayHead.find('th:first');
+ if (allDayTable) {
+ axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
+ }
+ axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
+
+ axisWidth = 0;
+ setOuterWidth(
+ axisFirstCells
+ .width('')
+ .each(function(i, _cell) {
+ axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
+ }),
+ axisWidth
+ );
+
+ var gutterCells = dayTable.find('.fc-agenda-gutter');
+ if (allDayTable) {
+ gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
+ }
+
+ var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
+
+ gutterWidth = slotScroller.width() - slotTableWidth;
+ if (gutterWidth) {
+ setOuterWidth(gutterCells, gutterWidth);
+ gutterCells
+ .show()
+ .prev()
+ .removeClass('fc-last');
+ }else{
+ gutterCells
+ .hide()
+ .prev()
+ .addClass('fc-last');
+ }
+
+ colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
+ setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
+ }
+
+
+
+ /* Scrolling
+ -----------------------------------------------------------------------*/
+
+
+ function resetScroll() {
+ var d0 = zeroDate();
+ var scrollDate = cloneDate(d0);
+ scrollDate.setHours(opt('firstHour'));
+ var top = timePosition(d0, scrollDate) + 1; // +1 for the border
+ function scroll() {
+ slotScroller.scrollTop(top);
+ }
+ scroll();
+ setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
+ }
+
+
+ function afterRender() { // after the view has been freshly rendered and sized
+ resetScroll();
+ }
+
+
+
+ /* Slot/Day clicking and binding
+ -----------------------------------------------------------------------*/
+
+
+ function dayBind(cells) {
+ cells.click(slotClick)
+ .mousedown(daySelectionMousedown);
+ }
+
+
+ function slotBind(cells) {
+ cells.click(slotClick)
+ .mousedown(slotSelectionMousedown);
+ }
+
+
+ function slotClick(ev) {
+ if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
+ var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
+ var date = cellToDate(0, col);
+ var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
+ if (rowMatch) {
+ var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
+ var hours = Math.floor(mins/60);
+ date.setHours(hours);
+ date.setMinutes(mins%60 + minMinute);
+ trigger('dayClick', dayBodyCells[col], date, false, ev);
+ }else{
+ trigger('dayClick', dayBodyCells[col], date, true, ev);
+ }
+ }
+ }
+
+
+
+ /* Semi-transparent Overlay Helpers
+ -----------------------------------------------------*/
+ // TODO: should be consolidated with BasicView's methods
+
+
+ function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
+
+ if (refreshCoordinateGrid) {
+ coordinateGrid.build();
+ }
+
+ var segments = rangeToSegments(overlayStart, overlayEnd);
+
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ dayBind(
+ renderCellOverlay(
+ segment.row,
+ segment.leftCol,
+ segment.row,
+ segment.rightCol
+ )
+ );
+ }
+ }
+
+
+ function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
+ var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
+ return renderOverlay(rect, slotLayer);
+ }
+
+
+ function renderSlotOverlay(overlayStart, overlayEnd) {
+ for (var i=0; i<colCnt; i++) {
+ var dayStart = cellToDate(0, i);
+ var dayEnd = addDays(cloneDate(dayStart), 1);
+ var stretchStart = new Date(Math.max(dayStart, overlayStart));
+ var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
+ if (stretchStart < stretchEnd) {
+ var rect = coordinateGrid.rect(0, i, 0, i, slotContainer); // only use it for horizontal coords
+ var top = timePosition(dayStart, stretchStart);
+ var bottom = timePosition(dayStart, stretchEnd);
+ rect.top = top;
+ rect.height = bottom - top;
+ slotBind(
+ renderOverlay(rect, slotContainer)
+ );
+ }
+ }
+ }
+
+
+
+ /* Coordinate Utilities
+ -----------------------------------------------------------------------------*/
+
+
+ coordinateGrid = new CoordinateGrid(function(rows, cols) {
+ var e, n, p;
+ dayHeadCells.each(function(i, _e) {
+ e = $(_e);
+ n = e.offset().left;
+ if (i) {
+ p[1] = n;
+ }
+ p = [n];
+ cols[i] = p;
+ });
+ p[1] = n + e.outerWidth();
+ if (opt('allDaySlot')) {
+ e = allDayRow;
+ n = e.offset().top;
+ rows[0] = [n, n+e.outerHeight()];
+ }
+ var slotTableTop = slotContainer.offset().top;
+ var slotScrollerTop = slotScroller.offset().top;
+ var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
+ function constrain(n) {
+ return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
+ }
+ for (var i=0; i<slotCnt*snapRatio; i++) { // adapt slot count to increased/decreased selection slot count
+ rows.push([
+ constrain(slotTableTop + snapHeight*i),
+ constrain(slotTableTop + snapHeight*(i+1))
+ ]);
+ }
+ });
+
+
+ hoverListener = new HoverListener(coordinateGrid);
+
+ colPositions = new HorizontalPositionCache(function(col) {
+ return dayBodyCellInners.eq(col);
+ });
+
+ colContentPositions = new HorizontalPositionCache(function(col) {
+ return dayBodyCellContentInners.eq(col);
+ });
+
+
+ function colLeft(col) {
+ return colPositions.left(col);
+ }
+
+
+ function colContentLeft(col) {
+ return colContentPositions.left(col);
+ }
+
+
+ function colRight(col) {
+ return colPositions.right(col);
+ }
+
+
+ function colContentRight(col) {
+ return colContentPositions.right(col);
+ }
+
+
+ function getIsCellAllDay(cell) {
+ return opt('allDaySlot') && !cell.row;
+ }
+
+
+ function realCellToDate(cell) { // ugh "real" ... but blame it on our abuse of the "cell" system
+ var d = cellToDate(0, cell.col);
+ var slotIndex = cell.row;
+ if (opt('allDaySlot')) {
+ slotIndex--;
+ }
+ if (slotIndex >= 0) {
+ addMinutes(d, minMinute + slotIndex * snapMinutes);
+ }
+ return d;
+ }
+
+
+ // get the Y coordinate of the given time on the given day (both Date objects)
+ function timePosition(day, time) { // both date objects. day holds 00:00 of current day
+ day = cloneDate(day, true);
+ if (time < addMinutes(cloneDate(day), minMinute)) {
+ return 0;
+ }
+ if (time >= addMinutes(cloneDate(day), maxMinute)) {
+ return slotTable.height();
+ }
+ var slotMinutes = opt('slotMinutes'),
+ minutes = time.getHours()*60 + time.getMinutes() - minMinute,
+ slotI = Math.floor(minutes / slotMinutes),
+ slotTop = slotTopCache[slotI];
+ if (slotTop === undefined) {
+ slotTop = slotTopCache[slotI] =
+ slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop;
+ // .eq() is faster than ":eq()" selector
+ // [0].offsetTop is faster than .position().top (do we really need this optimization?)
+ // a better optimization would be to cache all these divs
+ }
+ return Math.max(0, Math.round(
+ slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
+ ));
+ }
+
+
+ function getAllDayRow(index) {
+ return allDayRow;
+ }
+
+
+ function defaultEventEnd(event) {
+ var start = cloneDate(event.start);
+ if (event.allDay) {
+ return start;
+ }
+ return addMinutes(start, opt('defaultEventMinutes'));
+ }
+
+
+
+ /* Selection
+ ---------------------------------------------------------------------------------*/
+
+
+ function defaultSelectionEnd(startDate, allDay) {
+ if (allDay) {
+ return cloneDate(startDate);
+ }
+ return addMinutes(cloneDate(startDate), opt('slotMinutes'));
+ }
+
+
+ function renderSelection(startDate, endDate, allDay) { // only for all-day
+ if (allDay) {
+ if (opt('allDaySlot')) {
+ renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
+ }
+ }else{
+ renderSlotSelection(startDate, endDate);
+ }
+ }
+
+
+ function renderSlotSelection(startDate, endDate) {
+ var helperOption = opt('selectHelper');
+ coordinateGrid.build();
+ if (helperOption) {
+ var col = dateToCell(startDate).col;
+ if (col >= 0 && col < colCnt) { // only works when times are on same day
+ var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords
+ var top = timePosition(startDate, startDate);
+ var bottom = timePosition(startDate, endDate);
+ if (bottom > top) { // protect against selections that are entirely before or after visible range
+ rect.top = top;
+ rect.height = bottom - top;
+ rect.left += 2;
+ rect.width -= 5;
+ if ($.isFunction(helperOption)) {
+ var helperRes = helperOption(startDate, endDate);
+ if (helperRes) {
+ rect.position = 'absolute';
+ selectionHelper = $(helperRes)
+ .css(rect)
+ .appendTo(slotContainer);
+ }
+ }else{
+ rect.isStart = true; // conside rect a "seg" now
+ rect.isEnd = true; //
+ selectionHelper = $(slotSegHtml(
+ {
+ title: '',
+ start: startDate,
+ end: endDate,
+ className: ['fc-select-helper'],
+ editable: false
+ },
+ rect
+ ));
+ selectionHelper.css('opacity', opt('dragOpacity'));
+ }
+ if (selectionHelper) {
+ slotBind(selectionHelper);
+ slotContainer.append(selectionHelper);
+ setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
+ setOuterHeight(selectionHelper, rect.height, true);
+ }
+ }
+ }
+ }else{
+ renderSlotOverlay(startDate, endDate);
+ }
+ }
+
+
+ function clearSelection() {
+ clearOverlays();
+ if (selectionHelper) {
+ selectionHelper.remove();
+ selectionHelper = null;
+ }
+ }
+
+
+ function slotSelectionMousedown(ev) {
+ if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
+ unselect(ev);
+ var dates, helperOption = opt('selectHelper');
+ hoverListener.start(function(cell, origCell) {
+ clearSelection();
+ if (cell && (cell.col == origCell.col || !helperOption) && !getIsCellAllDay(cell)) {
+ var d1 = realCellToDate(origCell);
+ var d2 = realCellToDate(cell);
+ dates = [
+ d1,
+ addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes
+ d2,
+ addMinutes(cloneDate(d2), snapMinutes)
+ ].sort(dateCompare);
+ renderSlotSelection(dates[0], dates[3]);
+ }else{
+ dates = null;
+ }
+ }, ev);
+ $(document).one('mouseup', function(ev) {
+ hoverListener.stop();
+ if (dates) {
+ if (+dates[0] == +dates[1]) {
+ reportDayClick(dates[0], false, ev);
+ }
+ reportSelection(dates[0], dates[3], false, ev);
+ }
+ });
+ }
+ }
+
+
+ function reportDayClick(date, allDay, ev) {
+ trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev);
+ }
+
+
+
+ /* External Dragging
+ --------------------------------------------------------------------------------*/
+
+
+ function dragStart(_dragElement, ev, ui) {
+ hoverListener.start(function(cell) {
+ clearOverlays();
+ if (cell) {
+ if (getIsCellAllDay(cell)) {
+ renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
+ }else{
+ var d1 = realCellToDate(cell);
+ var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
+ renderSlotOverlay(d1, d2);
+ }
+ }
+ }, ev);
+ }
+
+
+ function dragStop(_dragElement, ev, ui) {
+ var cell = hoverListener.stop();
+ clearOverlays();
+ if (cell) {
+ trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui);
+ }
+ }
+
+
+}
+
+;;
+
+function AgendaEventRenderer() {
+ var t = this;
+
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.clearEvents = clearEvents;
+ t.slotSegHtml = slotSegHtml;
+
+
+ // imports
+ DayEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var isEventDraggable = t.isEventDraggable;
+ var isEventResizable = t.isEventResizable;
+ var eventEnd = t.eventEnd;
+ var eventElementHandlers = t.eventElementHandlers;
+ var setHeight = t.setHeight;
+ var getDaySegmentContainer = t.getDaySegmentContainer;
+ var getSlotSegmentContainer = t.getSlotSegmentContainer;
+ var getHoverListener = t.getHoverListener;
+ var getMaxMinute = t.getMaxMinute;
+ var getMinMinute = t.getMinMinute;
+ var timePosition = t.timePosition;
+ var getIsCellAllDay = t.getIsCellAllDay;
+ var colContentLeft = t.colContentLeft;
+ var colContentRight = t.colContentRight;
+ var cellToDate = t.cellToDate;
+ var getColCnt = t.getColCnt;
+ var getColWidth = t.getColWidth;
+ var getSnapHeight = t.getSnapHeight;
+ var getSnapMinutes = t.getSnapMinutes;
+ var getSlotContainer = t.getSlotContainer;
+ var reportEventElement = t.reportEventElement;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var eventDrop = t.eventDrop;
+ var eventResize = t.eventResize;
+ var renderDayOverlay = t.renderDayOverlay;
+ var clearOverlays = t.clearOverlays;
+ var renderDayEvents = t.renderDayEvents;
+ var calendar = t.calendar;
+ var formatDate = calendar.formatDate;
+ var formatDates = calendar.formatDates;
+ var timeLineInterval;
+
+
+ // overrides
+ t.draggableDayEvent = draggableDayEvent;
+
+
+ /* Rendering
+ ----------------------------------------------------------------------------*/
+
+
+ function renderEvents(events, modifiedEventId) {
+ var i, len=events.length,
+ dayEvents=[],
+ slotEvents=[];
+ for (i=0; i<len; i++) {
+ if (events[i].allDay) {
+ dayEvents.push(events[i]);
+ }else{
+ slotEvents.push(events[i]);
+ }
+ }
+
+ if (opt('allDaySlot')) {
+ renderDayEvents(dayEvents, modifiedEventId);
+ setHeight(); // no params means set to viewHeight
+ }
+
+ renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
+
+ if (opt('currentTimeIndicator')) {
+ window.clearInterval(timeLineInterval);
+ timeLineInterval = window.setInterval(setTimeIndicator, 30000);
+ setTimeIndicator();
+ }
+ }
+
+
+ function clearEvents() {
+ getDaySegmentContainer().empty();
+ getSlotSegmentContainer().empty();
+ }
+
+
+ function compileSlotSegs(events) {
+ var colCnt = getColCnt(),
+ minMinute = getMinMinute(),
+ maxMinute = getMaxMinute(),
+ d,
+ visEventEnds = $.map(events, slotEventEnd),
+ i,
+ j, seg,
+ colSegs,
+ segs = [];
+
+ for (i=0; i<colCnt; i++) {
+
+ d = cellToDate(0, i);
+ addMinutes(d, minMinute);
+
+ colSegs = sliceSegs(
+ events,
+ visEventEnds,
+ d,
+ addMinutes(cloneDate(d), maxMinute-minMinute)
+ );
+
+ colSegs = placeSlotSegs(colSegs); // returns a new order
+
+ for (j=0; j<colSegs.length; j++) {
+ seg = colSegs[j];
+ seg.col = i;
+ segs.push(seg);
+ }
+ }
+
+ return segs;
+ }
+
+
+ function sliceSegs(events, visEventEnds, start, end) {
+ var segs = [],
+ i, len=events.length, event,
+ eventStart, eventEnd,
+ segStart, segEnd,
+ isStart, isEnd;
+ for (i=0; i<len; i++) {
+ event = events[i];
+ eventStart = event.start;
+ eventEnd = visEventEnds[i];
+ if (eventEnd > start && eventStart < end) {
+ if (eventStart < start) {
+ segStart = cloneDate(start);
+ isStart = false;
+ }else{
+ segStart = eventStart;
+ isStart = true;
+ }
+ if (eventEnd > end) {
+ segEnd = cloneDate(end);
+ isEnd = false;
+ }else{
+ segEnd = eventEnd;
+ isEnd = true;
+ }
+ segs.push({
+ event: event,
+ start: segStart,
+ end: segEnd,
+ isStart: isStart,
+ isEnd: isEnd
+ });
+ }
+ }
+ return segs.sort(compareSlotSegs);
+ }
+
+
+ function slotEventEnd(event) {
+ if (event.end) {
+ return cloneDate(event.end);
+ }else{
+ return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
+ }
+ }
+
+
+ // renders events in the 'time slots' at the bottom
+ // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
+ // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
+
+ function renderSlotSegs(segs, modifiedEventId) {
+
+ var i, segCnt=segs.length, seg,
+ event,
+ top,
+ bottom,
+ columnLeft,
+ columnRight,
+ columnWidth,
+ width,
+ left,
+ right,
+ html = '',
+ eventElements,
+ eventElement,
+ triggerRes,
+ contentElement,
+ height,
+ slotSegmentContainer = getSlotSegmentContainer(),
+ isRTL = opt('isRTL'),
+ colCnt = getColCnt();
+
+ // calculate position/dimensions, create html
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ event = seg.event;
+ top = timePosition(seg.start, seg.start);
+ bottom = timePosition(seg.start, seg.end);
+ columnLeft = colContentLeft(seg.col);
+ columnRight = colContentRight(seg.col);
+ columnWidth = columnRight - columnLeft;
+
+ // shave off space on right near scrollbars (2.5%)
+ // TODO: move this to CSS somehow
+ columnRight -= columnWidth * .025;
+ columnWidth = columnRight - columnLeft;
+
+ width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
+
+ // bruederli@kolabsys.com: always disable slotEventOverlap in single day view
+ if (opt('slotEventOverlap') && colCnt > 1) {
+ // double the width while making sure resize handle is visible
+ // (assumed to be 20px wide)
+ width = Math.max(
+ (width - (20/2)) * 2,
+ width // narrow columns will want to make the segment smaller than
+ // the natural width. don't allow it
+ );
+ }
+
+ if (isRTL) {
+ right = columnRight - seg.backwardCoord * columnWidth;
+ left = right - width;
+ }
+ else {
+ left = columnLeft + seg.backwardCoord * columnWidth;
+ right = left + width;
+ }
+
+ // make sure horizontal coordinates are in bounds
+ left = Math.max(left, columnLeft);
+ right = Math.min(right, columnRight);
+ width = right - left;
+
+ seg.top = top;
+ seg.left = left;
+ seg.outerWidth = width;
+ seg.outerHeight = bottom - top;
+ html += slotSegHtml(event, seg);
+ }
+
+ slotSegmentContainer[0].innerHTML = html; // faster than html()
+ eventElements = slotSegmentContainer.children();
+
+ // retrieve elements, run through eventRender callback, bind event handlers
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ event = seg.event;
+ eventElement = $(eventElements[i]); // faster than eq()
+ triggerRes = trigger('eventRender', event, event, eventElement);
+ if (triggerRes === false) {
+ eventElement.remove();
+ }else{
+ if (triggerRes && triggerRes !== true) {
+ eventElement.remove();
+ eventElement = $(triggerRes)
+ .css({
+ position: 'absolute',
+ top: seg.top,
+ left: seg.left
+ })
+ .appendTo(slotSegmentContainer);
+ }
+ seg.element = eventElement;
+ if (event._id === modifiedEventId) {
+ bindSlotSeg(event, eventElement, seg);
+ }else{
+ eventElement[0]._fci = i; // for lazySegBind
+ }
+ reportEventElement(event, eventElement);
+ }
+ }
+
+ lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
+
+ // record event sides and title positions
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ if (eventElement = seg.element) {
+ seg.vsides = vsides(eventElement, true);
+ seg.hsides = hsides(eventElement, true);
+ contentElement = eventElement.find('.fc-event-content');
+ if (contentElement.length) {
+ seg.contentTop = contentElement[0].offsetTop;
+ }
+ }
+ }
+
+ // set all positions/dimensions at once
+ for (i=0; i<segCnt; i++) {
+ seg = segs[i];
+ if (eventElement = seg.element) {
+ eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
+ height = Math.max(0, seg.outerHeight - seg.vsides);
+ eventElement[0].style.height = height + 'px';
+ event = seg.event;
+ if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
+ // not enough room for title, put it in the time (TODO: maybe make both display:inline instead)
+ eventElement.find('div.fc-event-time')
+ .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
+ eventElement.find('div.fc-event-title')
+ .remove();
+ }
+ trigger('eventAfterRender', event, event, eventElement);
+ }
+ }
+
+ }
+
+
+ function slotSegHtml(event, seg) {
+ var html = "<";
+ var url = event.url;
+ var skinCss = getSkinCss(event, opt);
+ var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+ var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
+ if (isEventDraggable(event)) {
+ classes.push('fc-event-draggable');
+ }
+ if (seg.isStart) {
+ classes.push('fc-event-start');
+ }
+ if (seg.isEnd) {
+ classes.push('fc-event-end');
+ }
+ classes = classes.concat(event.className);
+ if (event.source) {
+ classes = classes.concat(event.source.className || []);
+ }
+ if (url) {
+ html += "a href='" + htmlEscape(event.url) + "'";
+ }else{
+ html += "div";
+ }
+ html +=
+ " class='" + classes.join(' ') + "'" +
+ " style=" +
+ "'" +
+ "position:absolute;" +
+ "top:" + seg.top + "px;" +
+ "left:" + seg.left + "px;" +
+ skinCss +
+ "'" +
+ " tabindex='0'>" +
+ "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-time'>" +
+ htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-content'>" +
+ "<div class='fc-event-title'>" +
+ htmlEscape(event.title || '') +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-bg'></div>" +
+ "</div>"; // close inner
+ if (seg.isEnd && isEventResizable(event)) {
+ html +=
+ "<div class='ui-resizable-handle ui-resizable-s' role='presentation'>=</div>";
+ }
+ html +=
+ "</" + (url ? "a" : "div") + ">";
+ return html;
+ }
+
+
+ function bindSlotSeg(event, eventElement, seg) {
+ var timeElement = eventElement.find('div.fc-event-time');
+ if (isEventDraggable(event)) {
+ draggableSlotEvent(event, eventElement, timeElement);
+ }
+ if (seg.isEnd && isEventResizable(event)) {
+ resizableSlotEvent(event, eventElement, timeElement);
+ }
+ eventElementHandlers(event, eventElement);
+ }
+
+
+ // draw a horizontal line indicating the current time (#143)
+ function setTimeIndicator()
+ {
+ var container = getSlotContainer();
+ var timeline = container.children('.fc-timeline');
+ if (timeline.length == 0) { // if timeline isn't there, add it
+ timeline = $('<hr>').addClass('fc-timeline').appendTo(container);
+ }
+
+ var cur_time = new Date();
+ if (t.visStart < cur_time && t.visEnd > cur_time) {
+ timeline.show();
+ }
+ else {
+ timeline.hide();
+ return;
+ }
+
+ var secs = (cur_time.getHours() * 60 * 60) + (cur_time.getMinutes() * 60) + cur_time.getSeconds();
+ var percents = secs / 86400; // 24 * 60 * 60 = 86400, # of seconds in a day
+
+ timeline.css('top', Math.floor(container.height() * percents - 1) + 'px');
+
+ if (t.name == 'agendaWeek') { // week view, don't want the timeline to go the whole way across
+ var daycol = $('.fc-today', t.element);
+ var left = daycol.position().left + 1;
+ var width = daycol.width();
+ timeline.css({ left: left + 'px', width: width + 'px' });
+ }
+ }
+
+
+ /* Dragging
+ -----------------------------------------------------------------------------------*/
+
+
+ // when event starts out FULL-DAY
+ // overrides DayEventRenderer's version because it needs to account for dragging elements
+ // to and from the slot area.
+
+ function draggableDayEvent(event, eventElement, seg) {
+ var isStart = seg.isStart;
+ var origWidth;
+ var revert;
+ var allDay = true;
+ var dayDelta;
+ var hoverListener = getHoverListener();
+ var colWidth = getColWidth();
+ var snapHeight = getSnapHeight();
+ var snapMinutes = getSnapMinutes();
+ var minMinute = getMinMinute();
+ eventElement.draggable({
+ opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
+ revertDuration: opt('dragRevertDuration'),
+ start: function(ev, ui) {
+ trigger('eventDragStart', eventElement, event, ev, ui);
+ hideEvents(event, eventElement);
+ origWidth = eventElement.width();
+ hoverListener.start(function(cell, origCell) {
+ clearOverlays();
+ if (cell) {
+ revert = false;
+ var origDate = cellToDate(0, origCell.col);
+ var date = cellToDate(0, cell.col);
+ dayDelta = dayDiff(date, origDate);
+ if (!cell.row) {
+ // on full-days
+ renderDayOverlay(
+ addDays(cloneDate(event.start), dayDelta),
+ addDays(exclEndDay(event), dayDelta)
+ );
+ resetElement();
+ }else{
+ // mouse is over bottom slots
+ if (isStart) {
+ if (allDay) {
+ // convert event to temporary slot-event
+ eventElement.width(colWidth - 10); // don't use entire width
+ setOuterHeight(
+ eventElement,
+ snapHeight * Math.round(
+ (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) /
+ snapMinutes
+ )
+ );
+ eventElement.draggable('option', 'grid', [colWidth, 1]);
+ allDay = false;
+ }
+ }else{
+ revert = true;
+ }
+ }
+ revert = revert || (allDay && !dayDelta);
+ }else{
+ resetElement();
+ revert = true;
+ }
+ eventElement.draggable('option', 'revert', revert);
+ }, ev, 'drag');
+ },
+ stop: function(ev, ui) {
+ hoverListener.stop();
+ clearOverlays();
+ trigger('eventDragStop', eventElement, event, ev, ui);
+ if (revert) {
+ // hasn't moved or is out of bounds (draggable has already reverted)
+ resetElement();
+ eventElement.css('filter', ''); // clear IE opacity side-effects
+ showEvents(event, eventElement);
+ }else{
+ // changed!
+ var minuteDelta = 0;
+ if (!allDay) {
+ minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight)
+ * snapMinutes
+ + minMinute
+ - (event.start.getHours() * 60 + event.start.getMinutes());
+ }
+ eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
+ }
+ }
+ });
+ function resetElement() {
+ if (!allDay) {
+ eventElement
+ .width(origWidth)
+ .height('')
+ .draggable('option', 'grid', null);
+ allDay = true;
+ }
+ }
+ }
+
+
+ // when event starts out IN TIMESLOTS
+
+ function draggableSlotEvent(event, eventElement, timeElement) {
+ var coordinateGrid = t.getCoordinateGrid();
+ var colCnt = getColCnt();
+ var colWidth = getColWidth();
+ var snapHeight = getSnapHeight();
+ var snapMinutes = getSnapMinutes();
+
+ // states
+ var origPosition; // original position of the element, not the mouse
+ var origCell;
+ var isInBounds, prevIsInBounds;
+ var isAllDay, prevIsAllDay;
+ var colDelta, prevColDelta;
+ var dayDelta; // derived from colDelta
+ var minuteDelta, prevMinuteDelta;
+
+ eventElement.draggable({
+ scroll: false,
+ grid: [ colWidth, snapHeight ],
+ axis: colCnt==1 ? 'y' : false,
+ opacity: opt('dragOpacity'),
+ revertDuration: opt('dragRevertDuration'),
+ start: function(ev, ui) {
+
+ trigger('eventDragStart', eventElement, event, ev, ui);
+ hideEvents(event, eventElement);
+
+ coordinateGrid.build();
+
+ // initialize states
+ origPosition = eventElement.position();
+ origCell = coordinateGrid.cell(ev.pageX, ev.pageY);
+ isInBounds = prevIsInBounds = true;
+ isAllDay = prevIsAllDay = getIsCellAllDay(origCell);
+ colDelta = prevColDelta = 0;
+ dayDelta = 0;
+ minuteDelta = prevMinuteDelta = 0;
+
+ },
+ drag: function(ev, ui) {
+
+ // NOTE: this `cell` value is only useful for determining in-bounds and all-day.
+ // Bad for anything else due to the discrepancy between the mouse position and the
+ // element position while snapping. (problem revealed in PR #55)
+ //
+ // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event.
+ // We should overhaul the dragging system and stop relying on jQuery UI.
+ var cell = coordinateGrid.cell(ev.pageX, ev.pageY);
+
+ // update states
+ isInBounds = !!cell;
+ if (isInBounds) {
+ isAllDay = getIsCellAllDay(cell);
+
+ // calculate column delta
+ colDelta = Math.round((ui.position.left - origPosition.left) / colWidth);
+ if (colDelta != prevColDelta) {
+ // calculate the day delta based off of the original clicked column and the column delta
+ var origDate = cellToDate(0, origCell.col);
+ var col = origCell.col + colDelta;
+ col = Math.max(0, col);
+ col = Math.min(colCnt-1, col);
+ var date = cellToDate(0, col);
+ dayDelta = dayDiff(date, origDate);
+ }
+
+ // calculate minute delta (only if over slots)
+ if (!isAllDay) {
+ minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes;
+ }
+ }
+
+ // any state changes?
+ if (
+ isInBounds != prevIsInBounds ||
+ isAllDay != prevIsAllDay ||
+ colDelta != prevColDelta ||
+ minuteDelta != prevMinuteDelta
+ ) {
+
+ updateUI();
+
+ // update previous states for next time
+ prevIsInBounds = isInBounds;
+ prevIsAllDay = isAllDay;
+ prevColDelta = colDelta;
+ prevMinuteDelta = minuteDelta;
+ }
+
+ // if out-of-bounds, revert when done, and vice versa.
+ eventElement.draggable('option', 'revert', !isInBounds);
+
+ },
+ stop: function(ev, ui) {
+
+ clearOverlays();
+ trigger('eventDragStop', eventElement, event, ev, ui);
+
+ if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed!
+ eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui);
+ }
+ else { // either no change or out-of-bounds (draggable has already reverted)
+
+ // reset states for next time, and for updateUI()
+ isInBounds = true;
+ isAllDay = false;
+ colDelta = 0;
+ dayDelta = 0;
+ minuteDelta = 0;
+
+ updateUI();
+ eventElement.css('filter', ''); // clear IE opacity side-effects
+
+ // sometimes fast drags make event revert to wrong position, so reset.
+ // also, if we dragged the element out of the area because of snapping,
+ // but the *mouse* is still in bounds, we need to reset the position.
+ eventElement.css(origPosition);
+
+ showEvents(event, eventElement);
+ }
+ }
+ });
+
+ function updateUI() {
+ clearOverlays();
+ if (isInBounds) {
+ if (isAllDay) {
+ timeElement.hide();
+ eventElement.draggable('option', 'grid', null); // disable grid snapping
+ renderDayOverlay(
+ addDays(cloneDate(event.start), dayDelta),
+ addDays(exclEndDay(event), dayDelta)
+ );
+ }
+ else {
+ updateTimeText(minuteDelta);
+ timeElement.css('display', ''); // show() was causing display=inline
+ eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping
+ }
+ }
+ }
+
+ function updateTimeText(minuteDelta) {
+ var newStart = addMinutes(cloneDate(event.start), minuteDelta);
+ var newEnd;
+ if (event.end) {
+ newEnd = addMinutes(cloneDate(event.end), minuteDelta);
+ }
+ timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
+ }
+
+ }
+
+
+
+ /* Resizing
+ --------------------------------------------------------------------------------------*/
+
+
+ function resizableSlotEvent(event, eventElement, timeElement) {
+ var snapDelta, prevSnapDelta;
+ var snapHeight = getSnapHeight();
+ var snapMinutes = getSnapMinutes();
+ eventElement.resizable({
+ handles: {
+ s: '.ui-resizable-handle'
+ },
+ grid: snapHeight,
+ start: function(ev, ui) {
+ snapDelta = prevSnapDelta = 0;
+ hideEvents(event, eventElement);
+ trigger('eventResizeStart', this, event, ev, ui);
+ },
+ resize: function(ev, ui) {
+ // don't rely on ui.size.height, doesn't take grid into account
+ snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight);
+ if (snapDelta != prevSnapDelta) {
+ timeElement.text(
+ formatDates(
+ event.start,
+ (!snapDelta && !event.end) ? null : // no change, so don't display time range
+ addMinutes(eventEnd(event), snapMinutes*snapDelta),
+ opt('timeFormat')
+ )
+ );
+ prevSnapDelta = snapDelta;
+ }
+ },
+ stop: function(ev, ui) {
+ trigger('eventResizeStop', this, event, ev, ui);
+ if (snapDelta) {
+ eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui);
+ }else{
+ showEvents(event, eventElement);
+ // BUG: if event was really short, need to put title back in span
+ }
+ }
+ });
+ }
+
+
+}
+
+
+
+/* Agenda Event Segment Utilities
+-----------------------------------------------------------------------------*/
+
+
+// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
+// list in the order they should be placed into the DOM (an implicit z-index).
+function placeSlotSegs(segs) {
+ var levels = buildSlotSegLevels(segs);
+ var level0 = levels[0];
+ var i;
+
+ computeForwardSlotSegs(levels);
+
+ if (level0) {
+
+ for (i=0; i<level0.length; i++) {
+ computeSlotSegPressures(level0[i]);
+ }
+
+ for (i=0; i<level0.length; i++) {
+ computeSlotSegCoords(level0[i], 0, 0);
+ }
+ }
+
+ return flattenSlotSegLevels(levels);
+}
+
+
+// Builds an array of segments "levels". The first level will be the leftmost tier of segments
+// if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
+function buildSlotSegLevels(segs) {
+ var levels = [];
+ var i, seg;
+ var j;
+
+ for (i=0; i<segs.length; i++) {
+ seg = segs[i];
+
+ // go through all the levels and stop on the first level where there are no collisions
+ for (j=0; j<levels.length; j++) {
+ if (!computeSlotSegCollisions(seg, levels[j]).length) {
+ break;
+ }
+ }
+
+ (levels[j] || (levels[j] = [])).push(seg);
+ }
+
+ return levels;
+}
+
+
+// For every segment, figure out the other segments that are in subsequent
+// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
+function computeForwardSlotSegs(levels) {
+ var i, level;
+ var j, seg;
+ var k;
+
+ for (i=0; i<levels.length; i++) {
+ level = levels[i];
+
+ for (j=0; j<level.length; j++) {
+ seg = level[j];
+
+ seg.forwardSegs = [];
+ for (k=i+1; k<levels.length; k++) {
+ computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
+ }
+ }
+ }
+}
+
+
+// Figure out which path forward (via seg.forwardSegs) results in the longest path until
+// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
+function computeSlotSegPressures(seg) {
+ var forwardSegs = seg.forwardSegs;
+ var forwardPressure = 0;
+ var i, forwardSeg;
+
+ if (seg.forwardPressure === undefined) { // not already computed
+
+ for (i=0; i<forwardSegs.length; i++) {
+ forwardSeg = forwardSegs[i];
+
+ // figure out the child's maximum forward path
+ computeSlotSegPressures(forwardSeg);
+
+ // either use the existing maximum, or use the child's forward pressure
+ // plus one (for the forwardSeg itself)
+ forwardPressure = Math.max(
+ forwardPressure,
+ 1 + forwardSeg.forwardPressure
+ );
+ }
+
+ seg.forwardPressure = forwardPressure;
+ }
+}
+
+
+// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
+// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
+// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
+//
+// The segment might be part of a "series", which means consecutive segments with the same pressure
+// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
+// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
+// coordinate of the first segment in the series.
+function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
+ var forwardSegs = seg.forwardSegs;
+ var i;
+
+ if (seg.forwardCoord === undefined) { // not already computed
+
+ if (!forwardSegs.length) {
+
+ // if there are no forward segments, this segment should butt up against the edge
+ seg.forwardCoord = 1;
+ }
+ else {
+
+ // sort highest pressure first
+ forwardSegs.sort(compareForwardSlotSegs);
+
+ // this segment's forwardCoord will be calculated from the backwardCoord of the
+ // highest-pressure forward segment.
+ computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
+ seg.forwardCoord = forwardSegs[0].backwardCoord;
+ }
+
+ // calculate the backwardCoord from the forwardCoord. consider the series
+ seg.backwardCoord = seg.forwardCoord -
+ (seg.forwardCoord - seriesBackwardCoord) / // available width for series
+ (seriesBackwardPressure + 1); // # of segments in the series
+
+ // use this segment's coordinates to computed the coordinates of the less-pressurized
+ // forward segments
+ for (i=0; i<forwardSegs.length; i++) {
+ computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
+ }
+ }
+}
+
+
+// Outputs a flat array of segments, from lowest to highest level
+function flattenSlotSegLevels(levels) {
+ var segs = [];
+ var i, level;
+ var j;
+
+ for (i=0; i<levels.length; i++) {
+ level = levels[i];
+
+ for (j=0; j<level.length; j++) {
+ segs.push(level[j]);
+ }
+ }
+
+ return segs;
+}
+
+
+// Find all the segments in `otherSegs` that vertically collide with `seg`.
+// Append into an optionally-supplied `results` array and return.
+function computeSlotSegCollisions(seg, otherSegs, results) {
+ results = results || [];
+
+ for (var i=0; i<otherSegs.length; i++) {
+ if (isSlotSegCollision(seg, otherSegs[i])) {
+ results.push(otherSegs[i]);
+ }
+ }
+
+ return results;
+}
+
+
+// Do these segments occupy the same vertical space?
+function isSlotSegCollision(seg1, seg2) {
+ return seg1.end > seg2.start && seg1.start < seg2.end;
+}
+
+
+// A cmp function for determining which forward segment to rely on more when computing coordinates.
+function compareForwardSlotSegs(seg1, seg2) {
+ // put higher-pressure first
+ return seg2.forwardPressure - seg1.forwardPressure ||
+ // put segments that are closer to initial edge first (and favor ones with no coords yet)
+ (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
+ // do normal sorting...
+ compareSlotSegs(seg1, seg2);
+}
+
+
+// A cmp function for determining which segment should be closer to the initial edge
+// (the left edge on a left-to-right calendar).
+function compareSlotSegs(seg1, seg2) {
+ return seg1.start - seg2.start || // earlier start time goes first
+ (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first
+ (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
+}
+
+
+;;
+
+/* Additional view: list (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+fcViews.list = ListView;
+
+
+function ListView(element, calendar) {
+ var t = this;
+
+ // exports
+ t.render = render;
+ t.select = dummy;
+ t.unselect = dummy;
+ t.reportSelection = dummy;
+ t.getDaySegmentContainer = function(){ return body; };
+
+ // imports
+ View.call(t, element, calendar, 'list');
+ ListEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var clearEvents = t.clearEvents;
+ var reportEventClear = t.reportEventClear;
+ var formatDates = calendar.formatDates;
+ var formatDate = calendar.formatDate;
+
+ // overrides
+ t.setWidth = setWidth;
+ t.setHeight = setHeight;
+
+ // locals
+ var body;
+ var firstDay;
+ var nwe;
+ var tm;
+ var colFormat;
+
+
+ function render(date, delta) {
+ if (delta) {
+ addDays(date, opt('listPage') * delta);
+ }
+ t.start = t.visStart = cloneDate(date, true);
+ t.end = addDays(cloneDate(t.start), opt('listPage'));
+ t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
+ addMinutes(t.visEnd, -1); // set end to 23:59
+ t.title = formatDates(date, t.visEnd, opt('titleFormat'));
+
+ updateOptions();
+
+ if (!body) {
+ buildSkeleton();
+ } else {
+ clearEvents();
+ }
+ }
+
+
+ function updateOptions() {
+ firstDay = opt('firstDay');
+ nwe = opt('weekends') ? 0 : 1;
+ tm = opt('theme') ? 'ui' : 'fc';
+ colFormat = opt('columnFormat', 'day');
+ }
+
+
+ function buildSkeleton() {
+ body = $('<div>').addClass('fc-list-content').appendTo(element);
+ }
+
+ function setHeight(height, dateChanged) {
+ if (!opt('listNoHeight'))
+ body.css('height', (height-1)+'px').css('overflow', 'auto');
+ }
+
+ function setWidth(width) {
+ // nothing to be done here
+ }
+
+ function dummy() {
+ // Stub.
+ }
+
+}
+
+;;
+
+/* Additional view renderer: list (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+function ListEventRenderer() {
+ var t = this;
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.renderEventTime = renderEventTime;
+ t.compileDaySegs = compileSegs; // for DayEventRenderer
+ t.clearEvents = clearEvents;
+ t.lazySegBind = lazySegBind;
+ t.sortCmp = sortCmp;
+
+ // imports
+ DayEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var reportEventElement = t.reportEventElement;
+ var eventElementHandlers = t.eventElementHandlers;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var getListContainer = t.getDaySegmentContainer;
+ var calendar = t.calendar;
+ var formatDate = calendar.formatDate;
+ var formatDates = calendar.formatDates;
+
+
+ /* Rendering
+ --------------------------------------------------------------------*/
+
+ function clearEvents() {
+ getListContainer().empty();
+ }
+
+ function renderEvents(events, modifiedEventId) {
+ events.sort(sortCmp);
+ clearEvents();
+ renderSegs(compileSegs(events), modifiedEventId);
+ }
+
+ function compileSegs(events) {
+ var segs = [];
+ var colFormat = opt('titleFormat', 'day');
+ var firstDay = opt('firstDay');
+ var segmode = opt('listSections');
+ var event, i, dd, wd, md, seg, segHash, curSegHash, segDate, curSeg = -1;
+ var today = clearTime(new Date());
+ var weekstart = addDays(cloneDate(today), -((today.getDay() - firstDay + 7) % 7));
+
+ for (i=0; i < events.length; i++) {
+ event = events[i];
+
+ // skip events out of range
+ if ((event.end || event.start) < t.start || event.start > t.visEnd)
+ continue;
+
+ // define sections of this event
+ // create smart sections such as today, tomorrow, this week, next week, next month, ect.
+ segDate = cloneDate(event.start < t.start && event.end > t.start ? t.start : event.start, true);
+ dd = dayDiff(segDate, today);
+ wd = Math.floor(dayDiff(segDate, weekstart) / 7);
+ md = segDate.getMonth() + ((segDate.getYear() - today.getYear()) * 12) - today.getMonth();
+
+ // build section title
+ if (segmode == 'smart') {
+ if (dd < 0) {
+ segHash = opt('listTexts', 'past');
+ } else if (dd == 0) {
+ segHash = opt('listTexts', 'today');
+ } else if (dd == 1) {
+ segHash = opt('listTexts', 'tomorrow');
+ } else if (wd == 0) {
+ segHash = opt('listTexts', 'thisWeek');
+ } else if (wd == 1) {
+ segHash = opt('listTexts', 'nextWeek');
+ } else if (md == 0) {
+ segHash = opt('listTexts', 'thisMonth');
+ } else if (md == 1) {
+ segHash = opt('listTexts', 'nextMonth');
+ } else if (md > 1) {
+ segHash = opt('listTexts', 'future');
+ }
+ } else if (segmode == 'month') {
+ segHash = formatDate(segDate, 'MMMM yyyy');
+ } else if (segmode == 'week') {
+ segHash = opt('listTexts', 'week') + formatDate(segDate, ' W');
+ } else if (segmode == 'day') {
+ segHash = formatDate(segDate, colFormat);
+ } else {
+ segHash = '';
+ }
+
+ // start new segment
+ if (segHash != curSegHash) {
+ segs[++curSeg] = { events: [], start: segDate, title: segHash, daydiff: dd, weekdiff: wd, monthdiff: md };
+ curSegHash = segHash;
+ }
+
+ segs[curSeg].events.push(event);
+ }
+
+ return segs;
+ }
+
+ function sortCmp(a, b) {
+ var sd = a.start.getTime() - b.start.getTime();
+ return sd || (a.end ? a.end.getTime() : 0) - (b.end ? b.end.getTime() : 0);
+ }
+
+ function renderSegs(segs, modifiedEventId) {
+ var tm = opt('theme') ? 'ui' : 'fc';
+ var headerClass = tm + "-widget-header";
+ var contentClass = tm + "-widget-content";
+ var i, j, seg, event, times, s, skinCss, skinCssAttr, classes, segContainer, eventElement, eventElements, triggerRes;
+
+ for (j=0; j < segs.length; j++) {
+ seg = segs[j];
+
+ if (seg.title) {
+ $('<div class="fc-list-header ' + headerClass + '">' + htmlEscape(seg.title) + '</div>').appendTo(getListContainer());
+ }
+ segContainer = $('<div>').addClass('fc-list-section ' + contentClass).appendTo(getListContainer());
+ s = '';
+
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ times = renderEventTime(event, seg);
+ skinCss = getSkinCss(event, opt);
+ skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+ classes = ['fc-event', 'fc-event-skin', 'fc-event-vert', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
+ if (event.source && event.source.className) {
+ classes = classes.concat(event.source.className);
+ }
+
+ s +=
+ "<div class='" + classes.join(' ') + "'" + skinCssAttr + ">" +
+ "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
+ "<div class='fc-event-time'>" +
+ (times[0] ? '<span class="fc-col-date">' + times[0] + '</span> ' : '') +
+ (times[1] ? '<span class="fc-col-time">' + times[1] + '</span>' : '') +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-content'>" +
+ "<div class='fc-event-title'>" +
+ htmlEscape(event.title) +
+ "</div>" +
+ "</div>" +
+ "<div class='fc-event-bg'></div>" +
+ "</div>" + // close inner
+ "</div>"; // close outer
+ }
+
+ segContainer[0].innerHTML = s;
+ eventElements = segContainer.children();
+
+ // retrieve elements, run through eventRender callback, bind event handlers
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ eventElement = $(eventElements[i]); // faster than eq()
+ triggerRes = trigger('eventRender', event, event, eventElement);
+ if (triggerRes === false) {
+ eventElement.remove();
+ } else {
+ if (triggerRes && triggerRes !== true) {
+ eventElement.remove();
+ eventElement = $(triggerRes).appendTo(segContainer);
+ }
+ if (event._id === modifiedEventId) {
+ eventElementHandlers(event, eventElement, seg);
+ } else {
+ eventElement[0]._fci = i; // for lazySegBind
+ }
+ reportEventElement(event, eventElement);
+ }
+ }
+
+ lazySegBind(segContainer, seg, eventElementHandlers);
+ }
+
+ markFirstLast(getListContainer());
+ }
+
+ // event time/date range to display
+ function renderEventTime(event, seg) {
+ var timeFormat = opt('timeFormat');
+ var dateFormat = opt('columnFormat');
+ var segmode = opt('listSections');
+ var duration = event.end ? event.end.getTime() - event.start.getTime() : 0;
+ var datestr = '', timestr = '';
+
+ if (segmode == 'smart') {
+ if (event.start < seg.start) {
+ datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat);
+ } else if (duration > DAY_MS) {
+ datestr = formatDates(event.start, event.end, dateFormat + '{ - ' + dateFormat + '}');
+ } else if (seg.daydiff == 0) {
+ datestr = opt('listTexts', 'today');
+ } else if (seg.daydiff == 1) {
+ datestr = opt('listTexts', 'tomorrow');
+ } else if (seg.weekdiff == 0 || seg.weekdiff == 1) {
+ datestr = formatDate(event.start, 'dddd');
+ } else if (seg.daydiff > 1 || seg.daydiff < 0) {
+ datestr = formatDate(event.start, dateFormat);
+ }
+ } else if (segmode != 'day') {
+ datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '{ - ' + dateFormat + '}' : ''));
+ }
+
+ if (!datestr && event.allDay) {
+ timestr = opt('allDayText');
+ } else if ((duration < DAY_MS || !datestr) && !event.allDay) {
+ timestr = formatDates(event.start, event.end, timeFormat);
+ }
+
+ return [datestr, timestr];
+ }
+
+ function lazySegBind(container, seg, bindHandlers) {
+ container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
+ var parent = ev.target, e = parent, i, event;
+ while (parent != this) {
+ e = parent;
+ parent = parent.parentNode;
+ }
+ if ((i = e._fci) !== undefined) {
+ e._fci = undefined;
+ event = seg.events[i];
+ bindHandlers(event, container.children().eq(i), seg);
+ $(ev.target).trigger(ev);
+ }
+ ev.stopPropagation();
+ });
+ }
+
+}
+
+
+;;
+
+/* Additional view: table (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+fcViews.table = TableView;
+
+
+function TableView(element, calendar) {
+ var t = this;
+
+ // exports
+ t.render = render;
+ t.select = dummy;
+ t.unselect = dummy;
+ t.getDaySegmentContainer = function(){ return table; };
+
+ // imports
+ View.call(t, element, calendar, 'table');
+ TableEventRenderer.call(t);
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var clearEvents = t.clearEvents;
+ var reportEventClear = t.reportEventClear;
+ var formatDates = calendar.formatDates;
+ var formatDate = calendar.formatDate;
+
+ // overrides
+ t.setWidth = setWidth;
+ t.setHeight = setHeight;
+
+ // locals
+ var div;
+ var table;
+ var firstDay;
+ var nwe;
+ var tm;
+ var colFormat;
+
+
+ function render(date, delta) {
+ if (delta) {
+ addDays(date, opt('listPage') * delta);
+ }
+ t.start = t.visStart = cloneDate(date, true);
+ t.end = addDays(cloneDate(t.start), opt('listPage'));
+ t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
+ addMinutes(t.visEnd, -1); // set end to 23:59
+ t.title = (t.visEnd.getTime() - t.visStart.getTime() < DAY_MS) ? formatDate(date, opt('titleFormat')) : formatDates(date, t.visEnd, opt('titleFormat'));
+
+ updateOptions();
+
+ if (!table) {
+ buildSkeleton();
+ } else {
+ clearEvents();
+ }
+ }
+
+
+ function updateOptions() {
+ firstDay = opt('firstDay');
+ nwe = opt('weekends') ? 0 : 1;
+ tm = opt('theme') ? 'ui' : 'fc';
+ colFormat = opt('columnFormat');
+ }
+
+
+ function buildSkeleton() {
+ var tableCols = opt('tableCols');
+ var s =
+ "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
+ "<colgroup>";
+ for (var c=0; c < tableCols.length; c++) {
+ s += "<col class='fc-event-" + tableCols[c] + "' />";
+ }
+ s += "</colgroup>" +
+ "</table>";
+ div = $('<div>').addClass('fc-list-content').appendTo(element);
+ table = $(s).appendTo(div);
+ }
+
+ function setHeight(height, dateChanged) {
+ if (!opt('listNoHeight'))
+ div.css('height', (height-1)+'px').css('overflow', 'auto');
+ }
+
+ function setWidth(width) {
+ // nothing to be done here
+ }
+
+ function dummy() {
+ // Stub.
+ }
+
+}
+
+;;
+
+/* Additional view renderer: table (by bruederli@kolabsys.com)
+---------------------------------------------------------------------------------*/
+
+function TableEventRenderer() {
+ var t = this;
+
+ // imports
+ ListEventRenderer.call(t);
+ var opt = t.opt;
+ var sortCmp = t.sortCmp;
+ var trigger = t.trigger;
+ var compileSegs = t.compileDaySegs;
+ var reportEventElement = t.reportEventElement;
+ var eventElementHandlers = t.eventElementHandlers;
+ var renderEventTime = t.renderEventTime;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var getListContainer = t.getDaySegmentContainer;
+ var lazySegBind = t.lazySegBind;
+ var calendar = t.calendar;
+ var formatDate = calendar.formatDate;
+ var formatDates = calendar.formatDates;
+
+ // exports
+ t.renderEvents = renderEvents;
+ t.clearEvents = clearEvents;
+
+
+ /* Rendering
+ --------------------------------------------------------------------*/
+
+ function clearEvents() {
+ getListContainer().children('tbody').remove();
+ }
+
+ function renderEvents(events, modifiedEventId) {
+ events.sort(sortCmp);
+ clearEvents();
+ renderSegs(compileSegs(events), modifiedEventId);
+ getListContainer().removeClass('fc-list-smart fc-list-day fc-list-month fc-list-week').addClass('fc-list-' + opt('listSections'));
+ }
+
+ function renderSegs(segs, modifiedEventId) {
+ var tm = opt('theme') ? 'ui' : 'fc';
+ var table = getListContainer();
+ var headerClass = tm + "-widget-header";
+ var contentClass = tm + "-widget-content";
+ var tableCols = opt('tableCols');
+ var timecol = $.inArray('time', tableCols) >= 0;
+ var i, j, seg, event, times, s, skinCss, skinCssAttr, skinClasses, rowClasses, segContainer, eventElements, eventElement, triggerRes;
+
+ for (j=0; j < segs.length; j++) {
+ seg = segs[j];
+
+ if (seg.title) {
+ $('<tbody class="fc-list-header"><tr><td class="fc-list-header ' + headerClass + '" colspan="' + tableCols.length + '">' + htmlEscape(seg.title) + '</td></tr></tbody>').appendTo(table);
+ }
+ segContainer = $('<tbody>').addClass('fc-list-section ' + contentClass).appendTo(table);
+ s = '';
+
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ times = renderEventTime(event, seg);
+ skinCss = getSkinCss(event, opt);
+ skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
+ skinClasses = ['fc-event-skin', 'fc-corner-left', 'fc-corner-right', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
+ if (event.source && event.source.className) {
+ skinClasses = skinClasses.concat(event.source.className);
+ }
+ rowClasses = ['fc-event', 'fc-event-row', 'fc-'+dayIDs[event.start.getDay()]].concat(event.className);
+ if (seg.daydiff == 0) {
+ rowClasses.push('fc-today');
+ }
+
+ s += "<tr class='" + rowClasses.join(' ') + "' tabindex='0'>";
+ for (var col, c=0; c < tableCols.length; c++) {
+ col = tableCols[c];
+ if (col == 'handle') {
+ s += "<td class='fc-event-handle'>" +
+ "<div class='" + skinClasses.join(' ') + "'" + skinCssAttr + ">" +
+ "<span class='fc-event-inner'></span>" +
+ "</div></td>";
+ } else if (col == 'date') {
+ s += "<td class='fc-event-date' colspan='" + (times[1] || !timecol ? 1 : 2) + "'>" + htmlEscape(times[0]) + "</td>";
+ } else if (col == 'time') {
+ if (times[1]) {
+ s += "<td class='fc-event-time'>" + htmlEscape(times[1]) + "</td>";
+ }
+ } else {
+ s += "<td class='fc-event-" + col + "'>" + (event[col] ? htmlEscape(event[col]) : '&nbsp;') + "</td>";
+ }
+ }
+ s += "</tr>";
+
+ // IE doesn't like innerHTML on tbody elements so we insert every row individually
+ if (document.all) {
+ $(s).appendTo(segContainer);
+ s = '';
+ }
+ }
+
+ if (!document.all)
+ segContainer[0].innerHTML = s;
+
+ eventElements = segContainer.children();
+
+ // retrieve elements, run through eventRender callback, bind event handlers
+ for (i=0; i < seg.events.length; i++) {
+ event = seg.events[i];
+ eventElement = $(eventElements[i]); // faster than eq()
+ triggerRes = trigger('eventRender', event, event, eventElement);
+ if (triggerRes === false) {
+ eventElement.remove();
+ } else {
+ if (triggerRes && triggerRes !== true) {
+ eventElement.remove();
+ eventElement = $(triggerRes).appendTo(segContainer);
+ }
+ if (event._id === modifiedEventId) {
+ eventElementHandlers(event, eventElement, seg);
+ } else {
+ eventElement[0]._fci = i; // for lazySegBind
+ }
+ reportEventElement(event, eventElement);
+ }
+ }
+
+ lazySegBind(segContainer, seg, eventElementHandlers);
+ markFirstLast(segContainer);
+ }
+
+ //markFirstLast(table);
+ }
+
+}
+;;
+
+
+function View(element, calendar, viewName) {
+ var t = this;
+
+
+ // exports
+ t.element = element;
+ t.calendar = calendar;
+ t.name = viewName;
+ t.opt = opt;
+ t.trigger = trigger;
+ t.isEventDraggable = isEventDraggable;
+ t.isEventResizable = isEventResizable;
+ t.setEventData = setEventData;
+ t.clearEventData = clearEventData;
+ t.eventEnd = eventEnd;
+ t.reportEventElement = reportEventElement;
+ t.triggerEventDestroy = triggerEventDestroy;
+ t.eventElementHandlers = eventElementHandlers;
+ t.showEvents = showEvents;
+ t.hideEvents = hideEvents;
+ t.eventDrop = eventDrop;
+ t.eventResize = eventResize;
+ // t.title
+ // t.start, t.end
+ // t.visStart, t.visEnd
+
+
+ // imports
+ var defaultEventEnd = t.defaultEventEnd;
+ var normalizeEvent = calendar.normalizeEvent; // in EventManager
+ var reportEventChange = calendar.reportEventChange;
+
+
+ // locals
+ var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
+ var eventElementsByID = {}; // eventID mapped to array of jQuery elements
+ var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
+ var options = calendar.options;
+
+
+
+ function opt(name, viewNameOverride) {
+ var v = options[name];
+ if ($.isPlainObject(v)) {
+ return smartProperty(v, viewNameOverride || viewName);
+ }
+ return v;
+ }
+
+
+ function trigger(name, thisObj) {
+ return calendar.trigger.apply(
+ calendar,
+ [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
+ );
+ }
+
+
+
+ /* Event Editable Boolean Calculations
+ ------------------------------------------------------------------------------*/
+
+
+ function isEventDraggable(event) {
+ var source = event.source || {};
+ return firstDefined(
+ event.startEditable,
+ source.startEditable,
+ opt('eventStartEditable'),
+ event.editable,
+ source.editable,
+ opt('editable')
+ )
+ && !opt('disableDragging'); // deprecated
+ }
+
+
+ function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
+ var source = event.source || {};
+ return firstDefined(
+ event.durationEditable,
+ source.durationEditable,
+ opt('eventDurationEditable'),
+ event.editable,
+ source.editable,
+ opt('editable')
+ )
+ && !opt('disableResizing'); // deprecated
+ }
+
+
+
+ /* Event Data
+ ------------------------------------------------------------------------------*/
+
+
+ function setEventData(events) { // events are already normalized at this point
+ eventsByID = {};
+ var i, len=events.length, event;
+ for (i=0; i<len; i++) {
+ event = events[i];
+ if (eventsByID[event._id]) {
+ eventsByID[event._id].push(event);
+ }else{
+ eventsByID[event._id] = [event];
+ }
+ }
+ }
+
+
+ function clearEventData() {
+ eventsByID = {};
+ eventElementsByID = {};
+ eventElementCouples = [];
+ }
+
+
+ // returns a Date object for an event's end
+ function eventEnd(event) {
+ return event.end ? cloneDate(event.end) : defaultEventEnd(event);
+ }
+
+
+
+ /* Event Elements
+ ------------------------------------------------------------------------------*/
+
+
+ // report when view creates an element for an event
+ function reportEventElement(event, element) {
+ eventElementCouples.push({ event: event, element: element });
+ if (eventElementsByID[event._id]) {
+ eventElementsByID[event._id].push(element);
+ }else{
+ eventElementsByID[event._id] = [element];
+ }
+ }
+
+
+ function triggerEventDestroy() {
+ $.each(eventElementCouples, function(i, couple) {
+ t.trigger('eventDestroy', couple.event, couple.event, couple.element);
+ });
+ }
+
+
+ // attaches eventClick, eventMouseover, eventMouseout
+ function eventElementHandlers(event, eventElement) {
+ eventElement
+ .click(function(ev) {
+ if (!eventElement.hasClass('ui-draggable-dragging') &&
+ !eventElement.hasClass('ui-resizable-resizing')) {
+ return trigger('eventClick', this, event, ev);
+ }
+ })
+ .hover(
+ function(ev) {
+ trigger('eventMouseover', this, event, ev);
+ },
+ function(ev) {
+ trigger('eventMouseout', this, event, ev);
+ }
+ )
+ .keypress(function(ev) {
+ if (ev.keyCode == 13)
+ $(this).trigger('click', { pointerType:'keyboard' });
+ });
+ // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
+ // TODO: same for resizing
+ }
+
+
+ function showEvents(event, exceptElement) {
+ eachEventElement(event, exceptElement, 'show');
+ }
+
+
+ function hideEvents(event, exceptElement) {
+ eachEventElement(event, exceptElement, 'hide');
+ }
+
+
+ function eachEventElement(event, exceptElement, funcName) {
+ // NOTE: there may be multiple events per ID (repeating events)
+ // and multiple segments per event
+ var elements = eventElementsByID[event._id],
+ i, len = elements.length;
+ for (i=0; i<len; i++) {
+ if (!exceptElement || elements[i][0] != exceptElement[0]) {
+ elements[i][funcName]();
+ }
+ }
+ }
+
+
+
+ /* Event Modification Reporting
+ ---------------------------------------------------------------------------------*/
+
+
+ function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
+ var oldAllDay = event.allDay;
+ var eventId = event._id;
+ moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
+ trigger(
+ 'eventDrop',
+ e,
+ event,
+ dayDelta,
+ minuteDelta,
+ allDay,
+ function() {
+ // TODO: investigate cases where this inverse technique might not work
+ moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
+ reportEventChange(eventId);
+ },
+ ev,
+ ui
+ );
+ reportEventChange(eventId);
+ }
+
+
+ function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
+ var eventId = event._id;
+ elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
+ trigger(
+ 'eventResize',
+ e,
+ event,
+ dayDelta,
+ minuteDelta,
+ function() {
+ // TODO: investigate cases where this inverse technique might not work
+ elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
+ reportEventChange(eventId);
+ },
+ ev,
+ ui
+ );
+ reportEventChange(eventId);
+ }
+
+
+
+ /* Event Modification Math
+ ---------------------------------------------------------------------------------*/
+
+
+ function moveEvents(events, dayDelta, minuteDelta, allDay) {
+ minuteDelta = minuteDelta || 0;
+ for (var e, len=events.length, i=0; i<len; i++) {
+ e = events[i];
+ if (allDay !== undefined) {
+ e.allDay = allDay;
+ }
+ addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
+ if (e.end) {
+ e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
+ }
+ normalizeEvent(e, options);
+ }
+ }
+
+
+ function elongateEvents(events, dayDelta, minuteDelta) {
+ minuteDelta = minuteDelta || 0;
+ for (var e, len=events.length, i=0; i<len; i++) {
+ e = events[i];
+ e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
+ normalizeEvent(e, options);
+ }
+ }
+
+
+
+ // ====================================================================================================
+ // Utilities for day "cells"
+ // ====================================================================================================
+ // The "basic" views are completely made up of day cells.
+ // The "agenda" views have day cells at the top "all day" slot.
+ // This was the obvious common place to put these utilities, but they should be abstracted out into
+ // a more meaningful class (like DayEventRenderer).
+ // ====================================================================================================
+
+
+ // For determining how a given "cell" translates into a "date":
+ //
+ // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
+ // Keep in mind that column indices are inverted with isRTL. This is taken into account.
+ //
+ // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
+ //
+ // 3. Convert the "day offset" into a "date" (a JavaScript Date object).
+ //
+ // The reverse transformation happens when transforming a date into a cell.
+
+
+ // exports
+ t.isHiddenDay = isHiddenDay;
+ t.skipHiddenDays = skipHiddenDays;
+ t.getCellsPerWeek = getCellsPerWeek;
+ t.dateToCell = dateToCell;
+ t.dateToDayOffset = dateToDayOffset;
+ t.dayOffsetToCellOffset = dayOffsetToCellOffset;
+ t.cellOffsetToCell = cellOffsetToCell;
+ t.cellToDate = cellToDate;
+ t.cellToCellOffset = cellToCellOffset;
+ t.cellOffsetToDayOffset = cellOffsetToDayOffset;
+ t.dayOffsetToDate = dayOffsetToDate;
+ t.rangeToSegments = rangeToSegments;
+
+
+ // internals
+ var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
+ var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
+ var cellsPerWeek;
+ var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
+ var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
+ var isRTL = opt('isRTL');
+
+
+ // initialize important internal variables
+ (function() {
+
+ if (opt('weekends') === false) {
+ hiddenDays.push(0, 6); // 0=sunday, 6=saturday
+ }
+
+ // Loop through a hypothetical week and determine which
+ // days-of-week are hidden. Record in both hashes (one is the reverse of the other).
+ for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
+ dayToCellMap[dayIndex] = cellIndex;
+ isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
+ if (!isHiddenDayHash[dayIndex]) {
+ cellToDayMap[cellIndex] = dayIndex;
+ cellIndex++;
+ }
+ }
+
+ cellsPerWeek = cellIndex;
+ if (!cellsPerWeek) {
+ throw 'invalid hiddenDays'; // all days were hidden? bad.
+ }
+
+ })();
+
+
+ // Is the current day hidden?
+ // `day` is a day-of-week index (0-6), or a Date object
+ function isHiddenDay(day) {
+ if (typeof day == 'object') {
+ day = day.getDay();
+ }
+ return isHiddenDayHash[day];
+ }
+
+
+ function getCellsPerWeek() {
+ return cellsPerWeek;
+ }
+
+
+ // Keep incrementing the current day until it is no longer a hidden day.
+ // If the initial value of `date` is not a hidden day, don't do anything.
+ // Pass `isExclusive` as `true` if you are dealing with an end date.
+ // `inc` defaults to `1` (increment one day forward each time)
+ function skipHiddenDays(date, inc, isExclusive) {
+ inc = inc || 1;
+ while (
+ isHiddenDayHash[ ( date.getDay() + (isExclusive ? inc : 0) + 7 ) % 7 ]
+ ) {
+ addDays(date, inc);
+ }
+ }
+
+
+ //
+ // TRANSFORMATIONS: cell -> cell offset -> day offset -> date
+ //
+
+ // cell -> date (combines all transformations)
+ // Possible arguments:
+ // - row, col
+ // - { row:#, col: # }
+ function cellToDate() {
+ var cellOffset = cellToCellOffset.apply(null, arguments);
+ var dayOffset = cellOffsetToDayOffset(cellOffset);
+ var date = dayOffsetToDate(dayOffset);
+ return date;
+ }
+
+ // cell -> cell offset
+ // Possible arguments:
+ // - row, col
+ // - { row:#, col:# }
+ function cellToCellOffset(row, col) {
+ var colCnt = t.getColCnt();
+
+ // rtl variables. wish we could pre-populate these. but where?
+ var dis = isRTL ? -1 : 1;
+ var dit = isRTL ? colCnt - 1 : 0;
+
+ if (typeof row == 'object') {
+ col = row.col;
+ row = row.row;
+ }
+ var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
+
+ return cellOffset;
+ }
+
+ // cell offset -> day offset
+ function cellOffsetToDayOffset(cellOffset) {
+ var day0 = t.visStart.getDay(); // first date's day of week
+ cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
+ return Math.floor(cellOffset / cellsPerWeek) * 7 // # of days from full weeks
+ + cellToDayMap[ // # of days from partial last week
+ (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
+ ]
+ - day0; // adjustment for beginning-of-week normalization
+ }
+
+ // day offset -> date (JavaScript Date object)
+ function dayOffsetToDate(dayOffset) {
+ var date = cloneDate(t.visStart);
+ addDays(date, dayOffset);
+ return date;
+ }
+
+
+ //
+ // TRANSFORMATIONS: date -> day offset -> cell offset -> cell
+ //
+
+ // date -> cell (combines all transformations)
+ function dateToCell(date) {
+ var dayOffset = dateToDayOffset(date);
+ var cellOffset = dayOffsetToCellOffset(dayOffset);
+ var cell = cellOffsetToCell(cellOffset);
+ return cell;
+ }
+
+ // date -> day offset
+ function dateToDayOffset(date) {
+ return dayDiff(date, t.visStart);
+ }
+
+ // day offset -> cell offset
+ function dayOffsetToCellOffset(dayOffset) {
+ var day0 = t.visStart.getDay(); // first date's day of week
+ dayOffset += day0; // normalize dayOffset to beginning-of-week
+ return Math.floor(dayOffset / 7) * cellsPerWeek // # of cells from full weeks
+ + dayToCellMap[ // # of cells from partial last week
+ (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
+ ]
+ - dayToCellMap[day0]; // adjustment for beginning-of-week normalization
+ }
+
+ // cell offset -> cell (object with row & col keys)
+ function cellOffsetToCell(cellOffset) {
+ var colCnt = t.getColCnt();
+
+ // rtl variables. wish we could pre-populate these. but where?
+ var dis = isRTL ? -1 : 1;
+ var dit = isRTL ? colCnt - 1 : 0;
+
+ var row = Math.floor(cellOffset / colCnt);
+ var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
+ return {
+ row: row,
+ col: col
+ };
+ }
+
+
+ //
+ // Converts a date range into an array of segment objects.
+ // "Segments" are horizontal stretches of time, sliced up by row.
+ // A segment object has the following properties:
+ // - row
+ // - cols
+ // - isStart
+ // - isEnd
+ //
+ function rangeToSegments(startDate, endDate) {
+ var rowCnt = t.getRowCnt();
+ var colCnt = t.getColCnt();
+ var segments = []; // array of segments to return
+
+ // day offset for given date range
+ var rangeDayOffsetStart = dateToDayOffset(startDate);
+ var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive
+
+ // first and last cell offset for the given date range
+ // "last" implies inclusivity
+ var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
+ var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
+
+ // loop through all the rows in the view
+ for (var row=0; row<rowCnt; row++) {
+
+ // first and last cell offset for the row
+ var rowCellOffsetFirst = row * colCnt;
+ var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
+
+ // get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
+ var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
+ var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
+
+ // make sure segment's offsets are valid and in view
+ if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
+
+ // translate to cells
+ var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
+ var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
+
+ // view might be RTL, so order by leftmost column
+ var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
+
+ // Determine if segment's first/last cell is the beginning/end of the date range.
+ // We need to compare "day offset" because "cell offsets" are often ambiguous and
+ // can translate to multiple days, and an edge case reveals itself when we the
+ // range's first cell is hidden (we don't want isStart to be true).
+ var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
+ var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
+
+ segments.push({
+ row: row,
+ leftCol: cols[0],
+ rightCol: cols[1],
+ isStart: isStart,
+ isEnd: isEnd
+ });
+ }
+ }
+
+ return segments;
+ }
+
+
+}
+
+;;
+
+function DayEventRenderer() {
+ var t = this;
+
+
+ // exports
+ t.renderDayEvents = renderDayEvents;
+ t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
+ t.resizableDayEvent = resizableDayEvent; // "
+
+
+ // imports
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var isEventDraggable = t.isEventDraggable;
+ var isEventResizable = t.isEventResizable;
+ var eventEnd = t.eventEnd;
+ var reportEventElement = t.reportEventElement;
+ var eventElementHandlers = t.eventElementHandlers;
+ var showEvents = t.showEvents;
+ var hideEvents = t.hideEvents;
+ var eventDrop = t.eventDrop;
+ var eventResize = t.eventResize;
+ var getRowCnt = t.getRowCnt;
+ var getColCnt = t.getColCnt;
+ var getColWidth = t.getColWidth;
+ var allDayRow = t.allDayRow; // TODO: rename
+ var colLeft = t.colLeft;
+ var colRight = t.colRight;
+ var colContentLeft = t.colContentLeft;
+ var colContentRight = t.colContentRight;
+ var dateToCell = t.dateToCell;
+ var getDaySegmentContainer = t.getDaySegmentContainer;
+ var formatDates = t.calendar.formatDates;
+ var renderDayOverlay = t.renderDayOverlay;
+ var clearOverlays = t.clearOverlays;
+ var clearSelection = t.clearSelection;
+ var getHoverListener = t.getHoverListener;
+ var rangeToSegments = t.rangeToSegments;
+ var cellToDate = t.cellToDate;
+ var cellToCellOffset = t.cellToCellOffset;
+ var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
+ var dateToDayOffset = t.dateToDayOffset;
+ var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
+
+
+ // Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
+ // Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
+ // Can only be called when the event container is empty (because it wipes out all innerHTML).
+ function renderDayEvents(events, modifiedEventId) {
+
+ // do the actual rendering. Receive the intermediate "segment" data structures.
+ var segments = _renderDayEvents(
+ events,
+ false, // don't append event elements
+ true // set the heights of the rows
+ );
+
+ // report the elements to the View, for general drag/resize utilities
+ segmentElementEach(segments, function(segment, element) {
+ reportEventElement(segment.event, element);
+ });
+
+ // attach mouse handlers
+ attachHandlers(segments, modifiedEventId);
+
+ // call `eventAfterRender` callback for each event
+ segmentElementEach(segments, function(segment, element) {
+ trigger('eventAfterRender', segment.event, segment.event, element);
+ });
+ }
+
+
+ // Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
+ // Append this event element to the event container, which might already be populated with events.
+ // If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
+ // This hack is used to maintain continuity when user is manually resizing an event.
+ // Returns an array of DOM elements for the event.
+ function renderTempDayEvent(event, adjustRow, adjustTop) {
+
+ // actually render the event. `true` for appending element to container.
+ // Recieve the intermediate "segment" data structures.
+ var segments = _renderDayEvents(
+ [ event ],
+ true, // append event elements
+ false // don't set the heights of the rows
+ );
+
+ var elements = [];
+
+ // Adjust certain elements' top coordinates
+ segmentElementEach(segments, function(segment, element) {
+ if (segment.row === adjustRow) {
+ element.css('top', adjustTop);
+ }
+ elements.push(element[0]); // accumulate DOM nodes
+ });
+
+ return elements;
+ }
+
+
+ // Render events onto the calendar. Only responsible for the VISUAL aspect.
+ // Not responsible for attaching handlers or calling callbacks.
+ // Set `doAppend` to `true` for rendering elements without clearing the existing container.
+ // Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
+ function _renderDayEvents(events, doAppend, doRowHeights) {
+
+ // where the DOM nodes will eventually end up
+ var finalContainer = getDaySegmentContainer();
+
+ // the container where the initial HTML will be rendered.
+ // If `doAppend`==true, uses a temporary container.
+ var renderContainer = doAppend ? $("<div/>") : finalContainer;
+
+ var segments = buildSegments(events);
+ var html;
+ var elements;
+
+ // calculate the desired `left` and `width` properties on each segment object
+ calculateHorizontals(segments);
+
+ // build the HTML string. relies on `left` property
+ html = buildHTML(segments);
+
+ // render the HTML. innerHTML is considerably faster than jQuery's .html()
+ renderContainer[0].innerHTML = html;
+
+ // retrieve the individual elements
+ elements = renderContainer.children();
+
+ // if we were appending, and thus using a temporary container,
+ // re-attach elements to the real container.
+ if (doAppend) {
+ finalContainer.append(elements);
+ }
+
+ // assigns each element to `segment.event`, after filtering them through user callbacks
+ resolveElements(segments, elements);
+
+ // Calculate the left and right padding+margin for each element.
+ // We need this for setting each element's desired outer width, because of the W3C box model.
+ // It's important we do this in a separate pass from acually setting the width on the DOM elements
+ // because alternating reading/writing dimensions causes reflow for every iteration.
+ segmentElementEach(segments, function(segment, element) {
+ segment.hsides = hsides(element, true); // include margins = `true`
+ });
+
+ // Set the width of each element
+ segmentElementEach(segments, function(segment, element) {
+ element.width(
+ Math.max(0, segment.outerWidth - segment.hsides)
+ );
+ });
+
+ // Grab each element's outerHeight (setVerticals uses this).
+ // To get an accurate reading, it's important to have each element's width explicitly set already.
+ segmentElementEach(segments, function(segment, element) {
+ segment.outerHeight = element.outerHeight(true); // include margins = `true`
+ });
+
+ // Set the top coordinate on each element (requires segment.outerHeight)
+ setVerticals(segments, doRowHeights);
+
+ return segments;
+ }
+
+
+ // Generate an array of "segments" for all events.
+ function buildSegments(events) {
+ var segments = [];
+ for (var i=0; i<events.length; i++) {
+ var eventSegments = buildSegmentsForEvent(events[i]);
+ segments.push.apply(segments, eventSegments); // append an array to an array
+ }
+ return segments;
+ }
+
+
+ // Generate an array of segments for a single event.
+ // A "segment" is the same data structure that View.rangeToSegments produces,
+ // with the addition of the `event` property being set to reference the original event.
+ function buildSegmentsForEvent(event) {
+ var startDate = event.start;
+ var endDate = exclEndDay(event);
+ var segments = rangeToSegments(startDate, endDate);
+ for (var i=0; i<segments.length; i++) {
+ segments[i].event = event;
+ }
+ return segments;
+ }
+
+
+ // Sets the `left` and `outerWidth` property of each segment.
+ // These values are the desired dimensions for the eventual DOM elements.
+ function calculateHorizontals(segments) {
+ var isRTL = opt('isRTL');
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+
+ // Determine functions used for calulating the elements left/right coordinates,
+ // depending on whether the view is RTL or not.
+ // NOTE:
+ // colLeft/colRight returns the coordinate butting up the edge of the cell.
+ // colContentLeft/colContentRight is indented a little bit from the edge.
+ var leftFunc = (isRTL ? segment.isEnd : segment.isStart) ? colContentLeft : colLeft;
+ var rightFunc = (isRTL ? segment.isStart : segment.isEnd) ? colContentRight : colRight;
+
+ var left = leftFunc(segment.leftCol);
+ var right = rightFunc(segment.rightCol);
+ segment.left = left;
+ segment.outerWidth = right - left;
+ }
+ }
+
+
+ // Build a concatenated HTML string for an array of segments
+ function buildHTML(segments) {
+ var html = '';
+ for (var i=0; i<segments.length; i++) {
+ html += buildHTMLForSegment(segments[i]);
+ }
+ return html;
+ }
+
+
+ // Build an HTML string for a single segment.
+ // Relies on the following properties:
+ // - `segment.event` (from `buildSegmentsForEvent`)
+ // - `segment.left` (from `calculateHorizontals`)
+ function buildHTMLForSegment(segment) {
+ var html = '';
+ var isRTL = opt('isRTL');
+ var event = segment.event;
+ var url = event.url;
+
+ // generate the list of CSS classNames
+ var classNames = [ 'fc-event', 'fc-event-skin', 'fc-event-hori' ];
+ if (isEventDraggable(event)) {
+ classNames.push('fc-event-draggable');
+ }
+ if (segment.isStart) {
+ classNames.push('fc-event-start');
+ }
+ if (segment.isEnd) {
+ classNames.push('fc-event-end');
+ }
+ // use the event's configured classNames
+ // guaranteed to be an array via `normalizeEvent`
+ classNames = classNames.concat(event.className);
+ if (event.source) {
+ // use the event's source's classNames, if specified
+ classNames = classNames.concat(event.source.className || []);
+ }
+
+ // generate a semicolon delimited CSS string for any of the "skin" properties
+ // of the event object (`backgroundColor`, `borderColor` and such)
+ var skinCss = getSkinCss(event, opt);
+
+ if (url) {
+ html += "<a href='" + htmlEscape(url) + "'";
+ }else{
+ html += "<div";
+ }
+ html +=
+ " class='" + classNames.join(' ') + "'" +
+ " style=" +
+ "'" +
+ "position:absolute;" +
+ "left:" + segment.left + "px;" +
+ skinCss +
+ "'" +
+ " tabindex='0'>" +
+ "<div class='fc-event-inner'>";
+ if (!event.allDay && segment.isStart) {
+ html +=
+ "<span class='fc-event-time'>" +
+ htmlEscape(
+ formatDates(event.start, event.end, opt('timeFormat'))
+ ) +
+ "</span>";
+ }
+ html +=
+ "<span class='fc-event-title'>" +
+ htmlEscape(event.title || '') +
+ "</span>" +
+ "</div>";
+ if (segment.isEnd && isEventResizable(event)) {
+ html +=
+ "<div class='ui-resizable-handle ui-resizable-" + (isRTL ? 'w' : 'e') + "'>" +
+ "&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
+ "</div>";
+ }
+ html += "</" + (url ? "a" : "div") + ">";
+
+ // TODO:
+ // When these elements are initially rendered, they will be briefly visibile on the screen,
+ // even though their widths/heights are not set.
+ // SOLUTION: initially set them as visibility:hidden ?
+
+ return html;
+ }
+
+
+ // Associate each segment (an object) with an element (a jQuery object),
+ // by setting each `segment.element`.
+ // Run each element through the `eventRender` filter, which allows developers to
+ // modify an existing element, supply a new one, or cancel rendering.
+ function resolveElements(segments, elements) {
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ var event = segment.event;
+ var element = elements.eq(i);
+
+ // call the trigger with the original element
+ var triggerRes = trigger('eventRender', event, event, element);
+
+ if (triggerRes === false) {
+ // if `false`, remove the event from the DOM and don't assign it to `segment.event`
+ element.remove();
+ }
+ else {
+ if (triggerRes && triggerRes !== true) {
+ // the trigger returned a new element, but not `true` (which means keep the existing element)
+
+ // re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
+ triggerRes = $(triggerRes)
+ .css({
+ position: 'absolute',
+ left: segment.left
+ });
+
+ element.replaceWith(triggerRes);
+ element = triggerRes;
+ }
+
+ segment.element = element;
+ }
+ }
+ }
+
+
+
+ /* Top-coordinate Methods
+ -------------------------------------------------------------------------------------------------*/
+
+
+ // Sets the "top" CSS property for each element.
+ // If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
+ // so that if elements vertically overflow, the cell expands vertically to compensate.
+ function setVerticals(segments, doRowHeights) {
+ var overflowLinks = {};
+ var rowContentHeights = calculateVerticals(segments, overflowLinks); // also sets segment.top
+ var rowContentElements = getRowContentElements(); // returns 1 inner div per row
+ var rowContentTops = [];
+
+ // Set each row's height by setting height of first inner div
+ if (doRowHeights) {
+ for (var i=0; i<rowContentElements.length; i++) {
+ rowContentElements[i].height(rowContentHeights[i]);
+ if (overflowLinks[i])
+ renderOverflowLinks(overflowLinks[i], rowContentElements[i]);
+ }
+ }
+
+ // Get each row's top, relative to the views's origin.
+ // Important to do this after setting each row's height.
+ for (var i=0; i<rowContentElements.length; i++) {
+ rowContentTops.push(
+ rowContentElements[i].position().top
+ );
+ }
+
+ // Set each segment element's CSS "top" property.
+ // Each segment object has a "top" property, which is relative to the row's top, but...
+ segmentElementEach(segments, function(segment, element) {
+ if (!segment.overflow) {
+ element.css(
+ 'top',
+ rowContentTops[segment.row] + segment.top // ...now, relative to views's origin
+ );
+ }
+ else {
+ element.hide();
+ }
+ });
+ }
+
+
+ // Calculate the "top" coordinate for each segment, relative to the "top" of the row.
+ // Also, return an array that contains the "content" height for each row
+ // (the height displaced by the vertically stacked events in the row).
+ // Requires segments to have their `outerHeight` property already set.
+ function calculateVerticals(segments, overflowLinks) {
+ var rowCnt = getRowCnt();
+ var colCnt = getColCnt();
+ var rowContentHeights = []; // content height for each row
+ var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row
+ var maxHeight = opt('maxHeight');
+ var top;
+
+ for (var rowI=0; rowI<rowCnt; rowI++) {
+ var segmentRow = segmentRows[rowI];
+
+ // an array of running total heights for each column.
+ // initialize with all zeros.
+ overflowLinks[rowI] = {};
+ var colHeights = [];
+ var overflows = [];
+ for (var colI=0; colI<colCnt; colI++) {
+ colHeights.push(0);
+ overflows.push(0);
+ }
+
+ // loop through every segment
+ for (var segmentI=0; segmentI<segmentRow.length; segmentI++) {
+ var segment = segmentRow[segmentI];
+
+ // find the segment's top coordinate by looking at the max height
+ // of all the columns the segment will be in.
+ top = arrayMax(
+ colHeights.slice(
+ segment.leftCol,
+ segment.rightCol + 1 // make exclusive for slice
+ )
+ );
+
+ if (maxHeight && top + segment.outerHeight > maxHeight) {
+ segment.overflow = true;
+ }
+ else {
+ segment.top = top;
+ top += segment.outerHeight;
+ }
+
+ // adjust the columns to account for the segment's height
+ for (var colI=segment.leftCol; colI<=segment.rightCol; colI++) {
+ if (overflows[colI]) {
+ segment.overflow = true;
+ }
+ if (segment.overflow) {
+ if (segment.isStart && !overflowLinks[rowI][colI])
+ overflowLinks[rowI][colI] = { seg:segment, top:top, date:cloneDate(segment.event.start, true), count:0 };
+ if (overflowLinks[rowI][colI])
+ overflowLinks[rowI][colI].count++;
+ overflows[colI]++;
+ }
+ else {
+ colHeights[colI] = top;
+ }
+ }
+ }
+
+ // the tallest column in the row should be the "content height"
+ rowContentHeights.push(arrayMax(colHeights));
+ }
+
+ return rowContentHeights;
+ }
+
+
+ // Build an array of segment arrays, each representing the segments that will
+ // be in a row of the grid, sorted by which event should be closest to the top.
+ function buildSegmentRows(segments) {
+ var rowCnt = getRowCnt();
+ var segmentRows = [];
+ var segmentI;
+ var segment;
+ var rowI;
+
+ // group segments by row
+ for (segmentI=0; segmentI<segments.length; segmentI++) {
+ segment = segments[segmentI];
+ rowI = segment.row;
+ if (segment.element) { // was rendered?
+ if (segmentRows[rowI]) {
+ // already other segments. append to array
+ segmentRows[rowI].push(segment);
+ }
+ else {
+ // first segment in row. create new array
+ segmentRows[rowI] = [ segment ];
+ }
+ }
+ }
+
+ // sort each row
+ for (rowI=0; rowI<rowCnt; rowI++) {
+ segmentRows[rowI] = sortSegmentRow(
+ segmentRows[rowI] || [] // guarantee an array, even if no segments
+ );
+ }
+
+ return segmentRows;
+ }
+
+
+ // Sort an array of segments according to which segment should appear closest to the top
+ function sortSegmentRow(segments) {
+ var sortedSegments = [];
+
+ // build the subrow array
+ var subrows = buildSegmentSubrows(segments);
+
+ // flatten it
+ for (var i=0; i<subrows.length; i++) {
+ sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
+ }
+
+ return sortedSegments;
+ }
+
+
+ // Take an array of segments, which are all assumed to be in the same row,
+ // and sort into subrows.
+ function buildSegmentSubrows(segments) {
+
+ // Give preference to elements with certain criteria, so they have
+ // a chance to be closer to the top.
+ segments.sort(compareDaySegments);
+
+ var subrows = [];
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+
+ // loop through subrows, starting with the topmost, until the segment
+ // doesn't collide with other segments.
+ for (var j=0; j<subrows.length; j++) {
+ if (!isDaySegmentCollision(segment, subrows[j])) {
+ break;
+ }
+ }
+ // `j` now holds the desired subrow index
+ if (subrows[j]) {
+ subrows[j].push(segment);
+ }
+ else {
+ subrows[j] = [ segment ];
+ }
+ }
+
+ return subrows;
+ }
+
+
+ // Return an array of jQuery objects for the placeholder content containers of each row.
+ // The content containers don't actually contain anything, but their dimensions should match
+ // the events that are overlaid on top.
+ function getRowContentElements() {
+ var i;
+ var rowCnt = getRowCnt();
+ var rowDivs = [];
+ for (i=0; i<rowCnt; i++) {
+ rowDivs[i] = allDayRow(i)
+ .find('div.fc-day-content > div');
+ }
+ return rowDivs;
+ }
+
+
+ function renderOverflowLinks(overflowLinks, rowDiv) {
+ var container = getDaySegmentContainer();
+ var colCnt = getColCnt();
+ var element, triggerRes, link;
+ for (var j=0; j<colCnt; j++) {
+ if ((link = overflowLinks[j])) {
+ if (link.count > 1) {
+ element = $('<a>').addClass('fc-more-link').html('+'+link.count).appendTo(container);
+ element[0].style.position = 'absolute';
+ element[0].style.left = link.seg.left + 'px';
+ element[0].style.top = (link.top + rowDiv[0].offsetTop) + 'px';
+ triggerRes = trigger('overflowRender', link, { count:link.count, date:link.date }, element);
+ if (triggerRes === false)
+ element.remove();
+ }
+ else {
+ link.seg.top = link.top;
+ link.seg.overflow = false;
+ }
+ }
+ }
+ }
+
+
+ /* Mouse Handlers
+ ---------------------------------------------------------------------------------------------------*/
+ // TODO: better documentation!
+
+
+ function attachHandlers(segments, modifiedEventId) {
+ var segmentContainer = getDaySegmentContainer();
+
+ segmentElementEach(segments, function(segment, element, i) {
+ var event = segment.event;
+ if (event._id === modifiedEventId) {
+ bindDaySeg(event, element, segment);
+ }else{
+ element[0]._fci = i; // for lazySegBind
+ }
+ });
+
+ lazySegBind(segmentContainer, segments, bindDaySeg);
+ }
+
+
+ function bindDaySeg(event, eventElement, segment) {
+
+ if (isEventDraggable(event)) {
+ t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
+ }
+
+ if (
+ segment.isEnd && // only allow resizing on the final segment for an event
+ isEventResizable(event)
+ ) {
+ t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
+ }
+
+ // attach all other handlers.
+ // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
+ eventElementHandlers(event, eventElement);
+ }
+
+
+ function draggableDayEvent(event, eventElement) {
+ var hoverListener = getHoverListener();
+ var dayDelta;
+ eventElement.draggable({
+ delay: 50,
+ opacity: opt('dragOpacity'),
+ revertDuration: opt('dragRevertDuration'),
+ start: function(ev, ui) {
+ trigger('eventDragStart', eventElement, event, ev, ui);
+ hideEvents(event, eventElement);
+ hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
+ eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
+ clearOverlays();
+ if (cell) {
+ var origDate = cellToDate(origCell);
+ var date = cellToDate(cell);
+ dayDelta = dayDiff(date, origDate);
+ renderDayOverlay(
+ addDays(cloneDate(event.start), dayDelta),
+ addDays(exclEndDay(event), dayDelta)
+ );
+ }else{
+ dayDelta = 0;
+ }
+ }, ev, 'drag');
+ },
+ stop: function(ev, ui) {
+ hoverListener.stop();
+ clearOverlays();
+ trigger('eventDragStop', eventElement, event, ev, ui);
+ if (dayDelta) {
+ eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
+ }else{
+ eventElement.css('filter', ''); // clear IE opacity side-effects
+ showEvents(event, eventElement);
+ }
+ }
+ });
+ }
+
+
+ function resizableDayEvent(event, element, segment) {
+ var isRTL = opt('isRTL');
+ var direction = isRTL ? 'w' : 'e';
+ var handle = element.find('.ui-resizable-' + direction); // TODO: stop using this class because we aren't using jqui for this
+ var isResizing = false;
+
+ // TODO: look into using jquery-ui mouse widget for this stuff
+ disableTextSelection(element); // prevent native <a> selection for IE
+ element
+ .mousedown(function(ev) { // prevent native <a> selection for others
+ ev.preventDefault();
+ })
+ .click(function(ev) {
+ if (isResizing) {
+ ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
+ ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
+ // (eventElementHandlers needs to be bound after resizableDayEvent)
+ }
+ });
+
+ handle.mousedown(function(ev) {
+ if (ev.which != 1) {
+ return; // needs to be left mouse button
+ }
+ isResizing = true;
+ var hoverListener = getHoverListener();
+ var rowCnt = getRowCnt();
+ var colCnt = getColCnt();
+ var elementTop = element.css('top');
+ var dayDelta;
+ var helpers;
+ var eventCopy = $.extend({}, event);
+ var minCellOffset = dayOffsetToCellOffset( dateToDayOffset(event.start) );
+ clearSelection();
+ $('body')
+ .css('cursor', direction + '-resize')
+ .one('mouseup', mouseup);
+ trigger('eventResizeStart', this, event, ev);
+ hoverListener.start(function(cell, origCell) {
+ if (cell) {
+
+ var origCellOffset = cellToCellOffset(origCell);
+ var cellOffset = cellToCellOffset(cell);
+
+ // don't let resizing move earlier than start date cell
+ cellOffset = Math.max(cellOffset, minCellOffset);
+
+ dayDelta =
+ cellOffsetToDayOffset(cellOffset) -
+ cellOffsetToDayOffset(origCellOffset);
+
+ if (dayDelta) {
+ eventCopy.end = addDays(eventEnd(event), dayDelta, true);
+ var oldHelpers = helpers;
+
+ helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
+ helpers = $(helpers); // turn array into a jQuery object
+
+ helpers.find('*').css('cursor', direction + '-resize');
+ if (oldHelpers) {
+ oldHelpers.remove();
+ }
+
+ hideEvents(event);
+ }
+ else {
+ if (helpers) {
+ showEvents(event);
+ helpers.remove();
+ helpers = null;
+ }
+ }
+ clearOverlays();
+ renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start()
+ event.start,
+ addDays( exclEndDay(event), dayDelta )
+ // TODO: instead of calling renderDayOverlay() with dates,
+ // call _renderDayOverlay (or whatever) with cell offsets.
+ );
+ }
+ }, ev);
+
+ function mouseup(ev) {
+ trigger('eventResizeStop', this, event, ev);
+ $('body').css('cursor', '');
+ hoverListener.stop();
+ clearOverlays();
+ if (dayDelta) {
+ eventResize(this, event, dayDelta, 0, ev);
+ // event redraw will clear helpers
+ }
+ // otherwise, the drag handler already restored the old events
+
+ setTimeout(function() { // make this happen after the element's click event
+ isResizing = false;
+ },0);
+ }
+ });
+ }
+
+
+}
+
+
+
+/* Generalized Segment Utilities
+-------------------------------------------------------------------------------------------------*/
+
+
+function isDaySegmentCollision(segment, otherSegments) {
+ for (var i=0; i<otherSegments.length; i++) {
+ var otherSegment = otherSegments[i];
+ if (
+ otherSegment.leftCol <= segment.rightCol &&
+ otherSegment.rightCol >= segment.leftCol
+ ) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
+ for (var i=0; i<segments.length; i++) {
+ var segment = segments[i];
+ var element = segment.element;
+ if (element) {
+ callback(segment, element, i);
+ }
+ }
+}
+
+
+// A cmp function for determining which segments should appear higher up
+function compareDaySegments(a, b) {
+ return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
+ b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
+ a.event.start - b.event.start || // if a tie, sort by event start date
+ (a.event.title || '').localeCompare(b.event.title) // if a tie, sort by event title
+}
+
+
+;;
+
+//BUG: unselect needs to be triggered when events are dragged+dropped
+
+function SelectionManager() {
+ var t = this;
+
+
+ // exports
+ t.select = select;
+ t.unselect = unselect;
+ t.reportSelection = reportSelection;
+ t.daySelectionMousedown = daySelectionMousedown;
+
+
+ // imports
+ var opt = t.opt;
+ var trigger = t.trigger;
+ var defaultSelectionEnd = t.defaultSelectionEnd;
+ var renderSelection = t.renderSelection;
+ var clearSelection = t.clearSelection;
+
+
+ // locals
+ var selected = false;
+
+
+
+ // unselectAuto
+ if (opt('selectable') && opt('unselectAuto')) {
+ $(document).mousedown(function(ev) {
+ var ignore = opt('unselectCancel');
+ if (ignore) {
+ if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
+ return;
+ }
+ }
+ unselect(ev);
+ });
+ }
+
+
+ function select(startDate, endDate, allDay) {
+ unselect();
+ if (!endDate) {
+ endDate = defaultSelectionEnd(startDate, allDay);
+ }
+ renderSelection(startDate, endDate, allDay);
+ reportSelection(startDate, endDate, allDay);
+ }
+
+
+ function unselect(ev) {
+ if (selected) {
+ selected = false;
+ clearSelection();
+ trigger('unselect', null, ev);
+ }
+ }
+
+
+ function reportSelection(startDate, endDate, allDay, ev) {
+ selected = true;
+ trigger('select', null, startDate, endDate, allDay, ev);
+ }
+
+
+ function daySelectionMousedown(ev) { // not really a generic manager method, oh well
+ var cellToDate = t.cellToDate;
+ var getIsCellAllDay = t.getIsCellAllDay;
+ var hoverListener = t.getHoverListener();
+ var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
+ if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
+ unselect(ev);
+ var _mousedownElement = this;
+ var dates;
+ hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
+ clearSelection();
+ if (cell && getIsCellAllDay(cell)) {
+ dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare);
+ renderSelection(dates[0], dates[1], true);
+ }else{
+ dates = null;
+ }
+ }, ev);
+ $(document).one('mouseup', function(ev) {
+ hoverListener.stop();
+ if (dates) {
+ if (+dates[0] == +dates[1]) {
+ reportDayClick(dates[0], true, ev);
+ }
+ reportSelection(dates[0], dates[1], true, ev);
+ }
+ });
+ }
+ }
+
+
+}
+
+;;
+
+function OverlayManager() {
+ var t = this;
+
+
+ // exports
+ t.renderOverlay = renderOverlay;
+ t.clearOverlays = clearOverlays;
+
+
+ // locals
+ var usedOverlays = [];
+ var unusedOverlays = [];
+
+
+ function renderOverlay(rect, parent) {
+ var e = unusedOverlays.shift();
+ if (!e) {
+ e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
+ }
+ if (e[0].parentNode != parent[0]) {
+ e.appendTo(parent);
+ }
+ usedOverlays.push(e.css(rect).show());
+ return e;
+ }
+
+
+ function clearOverlays() {
+ var e;
+ while (e = usedOverlays.shift()) {
+ unusedOverlays.push(e.hide().unbind());
+ }
+ }
+
+
+}
+
+;;
+
+function CoordinateGrid(buildFunc) {
+
+ var t = this;
+ var rows;
+ var cols;
+
+
+ t.build = function() {
+ rows = [];
+ cols = [];
+ buildFunc(rows, cols);
+ };
+
+
+ t.cell = function(x, y) {
+ var rowCnt = rows.length;
+ var colCnt = cols.length;
+ var i, r=-1, c=-1;
+ for (i=0; i<rowCnt; i++) {
+ if (y >= rows[i][0] && y < rows[i][1]) {
+ r = i;
+ break;
+ }
+ }
+ for (i=0; i<colCnt; i++) {
+ if (x >= cols[i][0] && x < cols[i][1]) {
+ c = i;
+ break;
+ }
+ }
+ return (r>=0 && c>=0) ? { row:r, col:c } : null;
+ };
+
+
+ t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
+ var origin = originElement.offset();
+ return {
+ top: rows[row0][0] - origin.top,
+ left: cols[col0][0] - origin.left,
+ width: cols[col1][1] - cols[col0][0],
+ height: rows[row1][1] - rows[row0][0]
+ };
+ };
+
+}
+
+;;
+
+function HoverListener(coordinateGrid) {
+
+
+ var t = this;
+ var bindType;
+ var change;
+ var firstCell;
+ var cell;
+
+
+ t.start = function(_change, ev, _bindType) {
+ change = _change;
+ firstCell = cell = null;
+ coordinateGrid.build();
+ mouse(ev);
+ bindType = _bindType || 'mousemove';
+ $(document).bind(bindType, mouse);
+ };
+
+
+ function mouse(ev) {
+ _fixUIEvent(ev); // see below
+ var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
+ if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
+ if (newCell) {
+ if (!firstCell) {
+ firstCell = newCell;
+ }
+ change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
+ }else{
+ change(newCell, firstCell);
+ }
+ cell = newCell;
+ }
+ }
+
+
+ t.stop = function() {
+ $(document).unbind(bindType, mouse);
+ return cell;
+ };
+
+
+}
+
+
+
+// this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
+// upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
+// but keep this in here for 1.8.16 users
+// and maybe remove it down the line
+
+function _fixUIEvent(event) { // for issue 1168
+ if (event.pageX === undefined) {
+ event.pageX = event.originalEvent.pageX;
+ event.pageY = event.originalEvent.pageY;
+ }
+}
+;;
+
+function HorizontalPositionCache(getElement) {
+
+ var t = this,
+ elements = {},
+ lefts = {},
+ rights = {};
+
+ function e(i) {
+ return elements[i] = elements[i] || getElement(i);
+ }
+
+ t.left = function(i) {
+ return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
+ };
+
+ t.right = function(i) {
+ return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
+ };
+
+ t.clear = function() {
+ elements = {};
+ lefts = {};
+ rights = {};
+ };
+
+}
+
+;;
+
+})(jQuery); \ No newline at end of file
diff --git a/calendar/localization/bg_BG.inc b/calendar/localization/bg_BG.inc
new file mode 100644
index 0000000..afbdfe1
--- /dev/null
+++ b/calendar/localization/bg_BG.inc
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Изглед по подразбиране';
+$labels['time_format'] = 'Формат на часовете';
+$labels['first_day'] = 'Първи ден от седмицата';
+$labels['first_hour'] = 'Първи час при показване';
+$labels['workinghours'] = 'Работни часове';
+$labels['add_category'] = 'Добавяне на категория';
+$labels['remove_category'] = 'Премахване на категория';
+$labels['defaultcalendar'] = 'Създаване на нови събития в ';
+$labels['eventcoloring'] = 'Оцветяване на събитията';
+$labels['coloringmode0'] = 'Според календара';
+$labels['coloringmode1'] = 'Относно категорията';
+$labels['coloringmode2'] = 'Календар за очертание, категория за съдържание';
+$labels['coloringmode3'] = 'Категория за очертание, календар за съдържание';
+$labels['calendar'] = 'Календар';
+$labels['calendars'] = 'Календари';
+$labels['category'] = 'Категория';
+$labels['categories'] = 'Категории';
+$labels['createcalendar'] = 'Създаване на нов календар';
+$labels['editcalendar'] = 'Промяна на свойствата на календара';
+$labels['name'] = 'Име';
+$labels['color'] = 'Цвят';
+$labels['day'] = 'Ден';
+$labels['week'] = 'Седмица';
+$labels['month'] = 'Месец';
+$labels['agenda'] = 'Бележник';
+$labels['new'] = 'Ново';
+$labels['new_event'] = 'Добавяне на събитие';
+$labels['edit_event'] = 'Промяна на събитие';
+$labels['edit'] = 'Промяна';
+$labels['save'] = 'Запис';
+$labels['cancel'] = 'Отказ';
+$labels['select'] = 'Избиране';
+$labels['print'] = 'Печат';
+$labels['printtitle'] = 'Печат на календарите';
+$labels['title'] = 'Заглавие';
+$labels['description'] = 'Описание';
+$labels['all-day'] = 'цял ден';
+$labels['export'] = 'Извличане';
+$labels['exporttitle'] = 'Извличане към iCalendar';
+$labels['exportrange'] = 'Събития от';
+$labels['location'] = 'Местоположение';
+$labels['date'] = 'Дата';
+$labels['start'] = 'Начало';
+$labels['end'] = 'Край';
+$labels['selectdate'] = 'Избор на дата';
+$labels['freebusy'] = 'Показване като';
+$labels['free'] = 'Свободно';
+$labels['busy'] = 'Заето';
+$labels['outofoffice'] = 'Извън офиса';
+$labels['tentative'] = 'Предварително';
+$labels['status'] = 'Статус';
+$labels['priority'] = 'Приоритет';
+$labels['sensitivity'] = 'Частност';
+$labels['public'] = 'публично';
+$labels['private'] = 'частно';
+$labels['confidential'] = 'конфиденциално';
+$labels['alarms'] = 'Напомняне';
+$labels['unknown'] = 'Неизвестно';
+$labels['generated'] = 'генерирано в';
+$labels['printdescriptions'] = 'Печат на описанията';
+$labels['parentcalendar'] = 'Внасяне вътре';
+$labels['searchearlierdates'] = '« Търсене за по- стари събития';
+$labels['searchlaterdates'] = 'Търсене за по- нови събития »';
+$labels['andnmore'] = '$nr повече...';
+$labels['createfrommail'] = 'Запазване като събитие';
+$labels['importevents'] = 'Внасяне на събития';
+$labels['importrange'] = 'Събития от';
+$labels['onemonthback'] = '1 месец назад';
+$labels['nmonthsback'] = '$nr месеца назад';
+$labels['showurl'] = 'Показване на URL на календара';
+$labels['showurldescription'] = 'Използвайте следния адрес, за да достъпвате (само за четене) вашия календар от други приложения. Можете да копирате и поставяте това във всеки календарен софтуер, поддържащ форматът iCal';
+$labels['listrange'] = 'Оразмеряване към екран:';
+$labels['listsections'] = 'Разделяне на:';
+$labels['smartsections'] = 'Интелигентни секции';
+$labels['until'] = 'до';
+$labels['today'] = 'Днес';
+$labels['tomorrow'] = 'Утре';
+$labels['thisweek'] = 'Тази седмица';
+$labels['nextweek'] = 'Следващата седмица';
+$labels['thismonth'] = 'Този месец';
+$labels['nextmonth'] = 'Следващия месец';
+$labels['weekofyear'] = 'Седмица';
+$labels['pastevents'] = 'Минали';
+$labels['futureevents'] = 'Бъдещи';
+$labels['defaultalarmtype'] = 'Настройка за напомняне по подразбиране';
+$labels['defaultalarmoffset'] = 'Време за напомняне по подразбиране';
+$labels['attendee'] = 'Участник';
+$labels['role'] = 'Роля';
+$labels['confirmstate'] = 'Статус';
+$labels['addattendee'] = 'Добавяне на участник';
+$labels['roleorganizer'] = 'Организатор';
+$labels['rolerequired'] = 'Задължителен';
+$labels['roleoptional'] = 'По избор';
+$labels['availfree'] = 'Свободно';
+$labels['availbusy'] = 'Заето';
+$labels['availunknown'] = 'Неизвестно';
+$labels['availtentative'] = 'Предварително';
+$labels['availoutofoffice'] = 'Извън офиса';
+$labels['sendinvitations'] = 'Изпращане на покани';
+$labels['sendnotifications'] = 'Известяване на участниците относно промените';
+$labels['sendcancellation'] = 'Известяване на участниците относно отмяна на събития';
+$labels['itipdeclineevent'] = 'Искате ли да отхвърлите поканата за това събитие?';
+$labels['saveincalendar'] = 'запазване в';
+$labels['tabsummary'] = 'Заглавие';
+$labels['tabattendees'] = 'Участници';
+$labels['tabattachments'] = 'Прикрепени файлове';
+$labels['tabsharing'] = 'Споделяне';
+$labels['deleteobjectconfirm'] = 'Наистина ли искате да премахнете това събитие?';
+$labels['deleteventconfirm'] = 'Наистина ли искате да премахнете това събитие?';
+$labels['deletecalendarconfirm'] = 'Наистина ли искате да премахнете този календар с всичките му събития?';
+$labels['savingdata'] = 'Запазване на данни...';
+$labels['errorsaving'] = 'Неуспешно записването на промените.';
+$labels['futurevents'] = 'Бъдещи';
+?>
diff --git a/calendar/localization/ca_ES.inc b/calendar/localization/ca_ES.inc
new file mode 100644
index 0000000..9226ef5
--- /dev/null
+++ b/calendar/localization/ca_ES.inc
@@ -0,0 +1,266 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Vista per defecte';
+$labels['time_format'] = 'Format de l\'hora';
+$labels['timeslots'] = 'Espais de temps per hora';
+$labels['first_day'] = 'Primer dia de la setmana';
+$labels['first_hour'] = 'Primera hora a mostrar';
+$labels['workinghours'] = 'Hores de feina';
+$labels['add_category'] = 'Afegeix categoria';
+$labels['remove_category'] = 'Suprimeix categoria';
+$labels['defaultcalendar'] = 'Crea nous esdeveniments a';
+$labels['eventcoloring'] = 'Colors dels esdeveniments';
+$labels['coloringmode0'] = 'Depenent del calendari';
+$labels['coloringmode1'] = 'Depenent de la categoria';
+$labels['coloringmode2'] = 'Calendari pel contorn, categoria pel contingut';
+$labels['coloringmode3'] = 'Categoria pel contorn, calendari pel contingut\'';
+$labels['afternothing'] = 'No facis res';
+$labels['aftertrash'] = 'Mou a la Paperera';
+$labels['afterdelete'] = 'Suprimeix el missatge';
+$labels['afterflagdeleted'] = 'Marca\'l com a suprimit';
+$labels['aftermoveto'] = 'Mou a...';
+$labels['itipoptions'] = 'Invitacions a esdeveniments';
+$labels['afteraction'] = 'S\'envia un missatge després d\'una invitació o una actualització';
+$labels['calendar'] = 'Calendari';
+$labels['calendars'] = 'Calendaris';
+$labels['category'] = 'Categoria';
+$labels['categories'] = 'Categories';
+$labels['createcalendar'] = 'Crea un nou calendari';
+$labels['editcalendar'] = 'Edita les propietats del calendari';
+$labels['name'] = 'Nom';
+$labels['color'] = 'Color';
+$labels['day'] = 'Dia';
+$labels['week'] = 'Setmana';
+$labels['month'] = 'Mes';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nou';
+$labels['new_event'] = 'Nou esdeveniment';
+$labels['edit_event'] = 'Edita esdeveniment';
+$labels['edit'] = 'Edita';
+$labels['save'] = 'Desa';
+$labels['removelist'] = 'Remove from list';
+$labels['cancel'] = 'Cancel·la';
+$labels['select'] = 'Selecciona';
+$labels['print'] = 'Imprimeix';
+$labels['printtitle'] = 'Imprimeix calendaris';
+$labels['title'] = 'Resum';
+$labels['description'] = 'Descripció';
+$labels['all-day'] = 'Tot el dia';
+$labels['export'] = 'Exporta';
+$labels['exporttitle'] = 'Exporta a iCalendari';
+$labels['exportrange'] = 'Esdeveniments de';
+$labels['exportattachments'] = 'Amb fitxers adjunts';
+$labels['customdate'] = 'Personalitza la data';
+$labels['location'] = 'Ubicació';
+$labels['url'] = 'URL';
+$labels['date'] = 'Data';
+$labels['start'] = 'Inici';
+$labels['starttime'] = 'Hora d\'inici';
+$labels['end'] = 'Final';
+$labels['endtime'] = 'Hora de finalització';
+$labels['repeat'] = 'Repeteix';
+$labels['selectdate'] = 'Tria la data';
+$labels['freebusy'] = 'Mostra\'m com';
+$labels['free'] = 'Lliure';
+$labels['busy'] = 'Ocupat';
+$labels['outofoffice'] = 'Fora de l\'oficina';
+$labels['tentative'] = 'Provisional';
+$labels['mystatus'] = 'El meu estat';
+$labels['status'] = 'Estat';
+$labels['status-confirmed'] = 'Confirmat';
+$labels['status-cancelled'] = 'Cancel·lat';
+$labels['priority'] = 'Prioritat';
+$labels['sensitivity'] = 'Privadesa';
+$labels['public'] = 'públic';
+$labels['private'] = 'privat';
+$labels['confidential'] = 'confidencial';
+$labels['links'] = 'Reference';
+$labels['alarms'] = 'Recordatori';
+$labels['comment'] = 'Comentari';
+$labels['created'] = 'Creat';
+$labels['changed'] = 'Darrera modificació';
+$labels['unknown'] = 'Desconegut';
+$labels['eventoptions'] = 'Opcions';
+$labels['generated'] = 'generat a';
+$labels['eventhistory'] = 'Historial';
+$labels['removelink'] = 'Remove email reference';
+$labels['printdescriptions'] = 'Imprimeix descripcions';
+$labels['parentcalendar'] = 'Insereix dins';
+$labels['searchearlierdates'] = '« Cerca els esdeveniments d\'abans';
+$labels['searchlaterdates'] = 'Cerca els esdeveniments de després »';
+$labels['andnmore'] = '$nr més...';
+$labels['togglerole'] = 'Feu clic per commutar el rol';
+$labels['createfrommail'] = 'Desa com a esdeveniment';
+$labels['importevents'] = 'Importa esdeveniments';
+$labels['importrange'] = 'Esdeveniments de';
+$labels['onemonthback'] = '1 mes abans';
+$labels['nmonthsback'] = '$nr mesos abans';
+$labels['showurl'] = 'Mostra la URL del calendari';
+$labels['showurldescription'] = 'Podeu fer servir aquesta adreça per accedir (només lectura) el vostre calendari des d\'altres aplicacions. Copieu i enganxeu-la dins d\'un altre programari de calendari que suporti el format iCal.';
+$labels['caldavurldescription'] = 'Copieu aquesta adreça a una aplicació client <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> (p.ex.: Evolution o Mozilla Thunderbird) per sincronitzar aquest calendari amb el vostre ordinador o dispositiu mòbil.';
+$labels['findcalendars'] = 'Cerca calendaris...';
+$labels['searchterms'] = 'Termes de cerca';
+$labels['calsearchresults'] = 'Calendaris disponibles';
+$labels['calendarsubscribe'] = 'Llista permanentment';
+$labels['nocalendarsfound'] = 'No s\'ha trobat cap calendari';
+$labels['nrcalendarsfound'] = 'S\'han trobat $nr calendaris';
+$labels['quickview'] = 'Mostra només aquest calendari';
+$labels['invitationspending'] = 'Invitacions pendents';
+$labels['invitationsdeclined'] = 'Invitacions declinades';
+$labels['changepartstat'] = 'Canvia l\'estat d\'un participant';
+$labels['rsvpcomment'] = 'Text d\'invitació';
+$labels['listrange'] = 'Rang per mostrar:';
+$labels['listsections'] = 'Divideix en:';
+$labels['smartsections'] = 'Seccions petites';
+$labels['until'] = 'fins';
+$labels['today'] = 'Avui';
+$labels['tomorrow'] = 'Demà';
+$labels['thisweek'] = 'Aquesta setmana';
+$labels['nextweek'] = 'Setmana vinent';
+$labels['prevweek'] = 'Setmana anterior';
+$labels['thismonth'] = 'Aquest mes';
+$labels['nextmonth'] = 'Mes vinent';
+$labels['weekofyear'] = 'Setmana';
+$labels['pastevents'] = 'Passat';
+$labels['futureevents'] = 'Futur';
+$labels['showalarms'] = 'Mostra els recordatoris';
+$labels['defaultalarmtype'] = 'Recordatori per defecte';
+$labels['defaultalarmoffset'] = 'Temps de recordatori per defecte';
+$labels['attendee'] = 'Participant';
+$labels['role'] = 'Rol';
+$labels['availability'] = 'Disp.';
+$labels['confirmstate'] = 'Estat';
+$labels['addattendee'] = 'Afegeix participant';
+$labels['roleorganizer'] = 'Organitzador';
+$labels['rolerequired'] = 'Obligatori';
+$labels['roleoptional'] = 'Opcional';
+$labels['rolechair'] = 'President';
+$labels['rolenonparticipant'] = 'Absent';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Grup';
+$labels['cutyperesource'] = 'Recurs';
+$labels['cutyperoom'] = 'Sala';
+$labels['availfree'] = 'Lliure';
+$labels['availbusy'] = 'Ocupat';
+$labels['availunknown'] = 'Desconegut';
+$labels['availtentative'] = 'Provisional';
+$labels['availoutofoffice'] = 'Fora de l\'oficina';
+$labels['delegatedto'] = 'Delegat a:';
+$labels['delegatedfrom'] = 'Delegat de:';
+$labels['scheduletime'] = 'Cerca disponibilitat';
+$labels['sendinvitations'] = 'Envia invitacions';
+$labels['sendnotifications'] = 'Notifica als participants quan hi hagi modificacions';
+$labels['sendcancellation'] = 'Notifica als participants si es cancel·la l\'esdeveniment';
+$labels['onlyworkinghours'] = 'Cerca disponibilitat dins de les hores de feina';
+$labels['reqallattendees'] = 'Obligatori/tots els participants';
+$labels['prevslot'] = 'Lloc anterior';
+$labels['nextslot'] = 'Lloc següent';
+$labels['suggestedslot'] = 'Lloc suggerit';
+$labels['noslotfound'] = 'No s\'ha pogut trobar un espai de temps lliure';
+$labels['invitationsubject'] = 'Heu estat convidats a "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nQuan: \$date\n\nConvidats: \$attendees\n\nSi us plau cerqueu el fitxer iCalendar adjunt dins dels detalls de l'esdeveniment per poder-lo importar a la vostra aplicació de calendari.";
+$labels['invitationattendlinks'] = "En cas que el vostre client de correu electrònic no suporti peticions de tipus iTip, podeu fer servir el següent enllaç per acceptar o declinar aquesta invitació:\n\$url";
+$labels['eventupdatesubject'] = '"$title" ha estat actualitzat';
+$labels['eventupdatesubjectempty'] = 'Un esdeveniment que us afecta ha estat actualitzat';
+$labels['eventupdatemailbody'] = "*\$title*\n\nQuan: \$date\n\nConvidats: \$attendees\n\nSi us plau cerqueu el fitxer iCalendar adjunt dins dels detalls actualitzats de l'esdeveniment per poder-lo importar a la vostra aplicació de calendari.";
+$labels['eventcancelsubject'] = '"$title" ha estat cancel·lat';
+$labels['eventcancelmailbody'] = "*\$title*\n\nQuan: \$date\n\nConvidats: \$attendees\n\nL'esdeveniment ha estat cancel·lat per \$organizer.\n\nSi us plau cerqueu el fitxer iCalendar adjunt amb els detalls actualitzats de l'esdeveniment.";
+$labels['itipobjectnotfound'] = 'L\'esdeveniment que fa referència aquest missatge no s\'ha trobat al vostre calendari.';
+$labels['itipmailbodyaccepted'] = "\$sender ha acceptat la invitació al següent esdeveniment:\n\n*\$title*\n\nQuan: \$date\n\nConvidats: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender ha acceptat provisionalment la invitació al següent esdeveniment:\n\n*\$title*\n\nQuan: \$date\n\nConvidats: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender ha declinat la invitació al següent esdeveniment:\n\n*\$title*\n\nQuan: \$date\n\nConvidats: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender ha rebutjat la vostra participació en el següent esdeveniment:\n\n*\$title*\n\nQuan: \$date";
+$labels['itipdeclineevent'] = 'Voleu declinar la vostra invitació a aquest esdeveniment?';
+$labels['declinedeleteconfirm'] = 'Voleu també suprimir aquest esdeveniment declinat del vostre calendari?';
+$labels['itipcomment'] = 'Comentari de la invitació/notificació';
+$labels['itipcommenttitle'] = 'Aquest comentari serà adjuntat al missatge d\'invitació/notificació que s\'envia als participants';
+$labels['notanattendee'] = 'No sou a la llista d\'assistents d\'aquest esdeveniment';
+$labels['eventcancelled'] = 'L\'esdeveniment s\'ha cancel·lat.';
+$labels['saveincalendar'] = 'desa a';
+$labels['updatemycopy'] = 'Actualitza en el meu calendari';
+$labels['savetocalendar'] = 'Desa al calendari';
+$labels['resource'] = 'Recurs';
+$labels['addresource'] = 'Recurs de llibre';
+$labels['findresources'] = 'Cerca recursos';
+$labels['resourcedetails'] = 'Detalls';
+$labels['resourceavailability'] = 'Disponibilitat';
+$labels['resourceowner'] = 'Propietari';
+$labels['resourceadded'] = 'El recurs ha estat afegit al vostre esdeveniment';
+$labels['tabsummary'] = 'Resum';
+$labels['tabrecurrence'] = 'Periodicitat';
+$labels['tabattendees'] = 'Participants';
+$labels['tabresources'] = 'Recursos';
+$labels['tabattachments'] = 'Fitxers adjunts';
+$labels['tabsharing'] = 'Compartit';
+$labels['deleteobjectconfirm'] = 'Esteu segur de voler suprimir aquest esdeveniment?';
+$labels['deleteventconfirm'] = 'Esteu segur de voler suprimir aquest esdeveniment?';
+$labels['deletecalendarconfirm'] = 'Esteu segurs de voler suprimir aquest calendari amb tots els seus esdeveniments?';
+$labels['deletecalendarconfirmrecursive'] = 'Esteu segurs de voler suprimir aquest calendari amb tots els seus esdeveniments i sub-calendaris?';
+$labels['savingdata'] = 'S\'estan desant les dades...';
+$labels['errorsaving'] = 'No s\'han pogut desar els canvis.';
+$labels['operationfailed'] = 'L\'operació sol·licitada ha fallat.';
+$labels['invalideventdates'] = 'Les dades entrades no són vàlides!. Si us plau verifiqueu l\'entrada.';
+$labels['invalidcalendarproperties'] = 'Les propietats del calendari no són vàlides!. Si us plau introduïu un nom vàlid.';
+$labels['searchnoresults'] = 'No s\'ha trobat cap esdeveniment en els calendaris seleccionats.';
+$labels['successremoval'] = 'L\'esdeveniment ha estat suprimit correctament.';
+$labels['successrestore'] = 'L\'esdeveniment ha estat recuperat correctament.';
+$labels['errornotifying'] = 'No s\'ha pogut enviar les notificacions als participants de l\'esdeveniment';
+$labels['errorimportingevent'] = 'No s\'ha pogut importar aquest esdeveniment';
+$labels['importwarningexists'] = 'Ja existeix una còpia d\'aquest esdeveniment al vostre calendari.';
+$labels['newerversionexists'] = 'Ja existeix una nova versió d\'aquest esdeveniment!. S\'ha avortat.';
+$labels['nowritecalendarfound'] = 'No s\'ha trobat el calendari per desar l\'esdeveniment';
+$labels['importedsuccessfully'] = 'L\'esdeveniment ha estat correctament afegit a \'$calendar\'';
+$labels['updatedsuccessfully'] = 'L\'esdeveniment ha estat correctament actualitzat dins de \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'L\'estat del participant ha estat actualitzat correctament';
+$labels['itipsendsuccess'] = 'La invitació ha estat enviada als participants.';
+$labels['itipresponseerror'] = 'No s\'ha pogut enviar la resposta a la invitació d\'aquest esdeveniment';
+$labels['itipinvalidrequest'] = 'Aquesta invitació ja no és vàlida';
+$labels['sentresponseto'] = 'S\'ha enviat correctament la resposta de la invitació a $mailto';
+$labels['localchangeswarning'] = 'Esteu a punt de fer canvis que només seran reflectits al vostre calendari i no seran enviats a l\'organitzador de l\'esdeveniment.';
+$labels['importsuccess'] = 'S\'han importat correctament $nr esdeveniments';
+$labels['importnone'] = 'No s\'ha trobat cap esdeveniment per importar';
+$labels['importerror'] = 'Hi ha hagut un error mentre s\'importava';
+$labels['aclnorights'] = 'No teniu drets d\'administrador en aquest calendari.';
+$labels['changeeventconfirm'] = 'Canvia l\'esdeveniment';
+$labels['changerecurringeventwarning'] = 'Aquest és un esdeveniment periòdic. Voleu editar només l\'esdeveniment actual, aquesta i totes les futures ocurrències, totes les ocurrències o desar-lo com un esdeveniment nou?';
+$labels['currentevent'] = 'Actual';
+$labels['futurevents'] = 'Futurs';
+$labels['allevents'] = 'Tots';
+$labels['saveasnew'] = 'Desa com a nou';
+$labels['birthdays'] = 'Aniversaris';
+$labels['birthdayscalendar'] = 'Calendari d\'aniversaris';
+$labels['displaybirthdayscalendar'] = 'Mostra el calendari d\'aniversaris';
+$labels['birthdayscalendarsources'] = 'D\'aquestes llibretes d\'adreces';
+$labels['birthdayeventtitle'] = 'Aniversari de $name';
+$labels['birthdayage'] = 'Edat $age';
+$labels['objectchangelog'] = 'Canvia historial';
+$labels['revision'] = 'Revisió';
+$labels['user'] = 'Usuari';
+$labels['operation'] = 'Acció';
+$labels['actionappend'] = 'Desat';
+$labels['actionmove'] = 'Mogut';
+$labels['actiondelete'] = 'Suprimit';
+$labels['compare'] = 'Compara';
+$labels['showrevision'] = 'Mostra aquesta versió';
+$labels['restore'] = 'Restaura aquesta versió';
+$labels['objectnotfound'] = 'No s\'han pogut carregar les dades d\'aquest esdeveniment';
+$labels['objectchangelognotavailable'] = 'No està disponible canviar l\'historial d\'aquest esdeveniment';
+$labels['objectdiffnotavailable'] = 'No és possible comparar les revisions seleccionades';
+$labels['revisionrestoreconfirm'] = 'Esteu segurs de voler restaurar la revisió $rev d\'aquest esdeveniment? Això substituirà l\'actual esdeveniment per una versió antiga.';
+$labels['arialabelminical'] = 'Selecció de la data del calendari';
+$labels['arialabelcalendarview'] = 'Vista del calendari';
+$labels['arialabelsearchform'] = 'Formulari per cercar esdeveniments';
+$labels['arialabelquicksearchbox'] = 'Entrada de cerca d\'esdeveniments';
+$labels['arialabelcalsearchform'] = 'Formulari de cerca de calendaris';
+$labels['calendaractions'] = 'Accions del calendari';
+$labels['arialabeleventattendees'] = 'Llista de participants de l\'esdeveniment';
+$labels['arialabeleventresources'] = 'Llista de recursos de l\'esdeveniment';
+$labels['arialabelresourcesearchform'] = 'Formulari de cerca de recursos';
+$labels['arialabelresourceselection'] = 'Recursos disponibles';
+?>
diff --git a/calendar/localization/cs_CZ.inc b/calendar/localization/cs_CZ.inc
new file mode 100644
index 0000000..8f47574
--- /dev/null
+++ b/calendar/localization/cs_CZ.inc
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Výchozí pohled';
+$labels['time_format'] = 'Formát data';
+$labels['timeslots'] = 'Míst v rozvrhu na hodinu';
+$labels['first_day'] = 'První den v týdnu';
+$labels['first_hour'] = 'První hodina k zobrazení';
+$labels['workinghours'] = 'Pracovní hodiny';
+$labels['add_category'] = 'Přidat kategorii';
+$labels['remove_category'] = 'Odstranit kategorii';
+$labels['defaultcalendar'] = 'Vytvářet nové události v';
+$labels['eventcoloring'] = 'Barvy událostí';
+$labels['coloringmode0'] = 'Podle kalendáře';
+$labels['coloringmode1'] = 'Podle kategorie';
+$labels['coloringmode2'] = 'Kalendář pro orámování, kategorie pro obsah';
+$labels['coloringmode3'] = 'Kategorie pro orámování, kalendář pro obsah';
+$labels['afternothing'] = 'Nedělat nic';
+$labels['aftertrash'] = 'Přesunout do koše';
+$labels['afterdelete'] = 'Smazat zprávu';
+$labels['afterflagdeleted'] = 'Označit jako smazané';
+$labels['aftermoveto'] = 'Přesunout do...';
+$labels['itipoptions'] = 'Pozvání na událost';
+$labels['afteraction'] = 'Poté co jsou pozvání nebo aktualizace zprávy zpracovány';
+$labels['calendar'] = 'Kalendář';
+$labels['calendars'] = 'Kalendáře';
+$labels['category'] = 'Kategorie';
+$labels['categories'] = 'Kategorie';
+$labels['createcalendar'] = 'Vytvořit nový kalendář';
+$labels['editcalendar'] = 'Upravit vlastnosti kalendáře';
+$labels['name'] = 'Název';
+$labels['color'] = 'Barva';
+$labels['day'] = 'Den';
+$labels['week'] = 'Týden';
+$labels['month'] = 'Měsíc';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nová';
+$labels['new_event'] = 'Nová událost';
+$labels['edit_event'] = 'Upravit událost';
+$labels['edit'] = 'Upravit';
+$labels['save'] = 'Uložit';
+$labels['removelist'] = 'Odstranit ze seznamu';
+$labels['cancel'] = 'Storno';
+$labels['select'] = 'Vybrat';
+$labels['print'] = 'Tisk';
+$labels['printtitle'] = 'Vytisknout kalendáře';
+$labels['title'] = 'Souhrn';
+$labels['description'] = 'Popis';
+$labels['all-day'] = 'celý den';
+$labels['export'] = 'Uložit jako ICS';
+$labels['exporttitle'] = 'Uložit jako iCalendar';
+$labels['exportrange'] = 'Události od';
+$labels['exportattachments'] = 'S přílohami';
+$labels['customdate'] = 'Vlastní datum';
+$labels['location'] = 'Místo';
+$labels['url'] = 'URL';
+$labels['date'] = 'Datum';
+$labels['start'] = 'Začátek';
+$labels['starttime'] = 'Začáteční čas';
+$labels['end'] = 'Konec';
+$labels['endtime'] = 'Čas konce';
+$labels['repeat'] = 'Opakování';
+$labels['selectdate'] = 'Vyberte datum';
+$labels['freebusy'] = 'Zobrazovat mě jako';
+$labels['free'] = 'volno';
+$labels['busy'] = 'obsazeno';
+$labels['outofoffice'] = 'mimo kancelář';
+$labels['tentative'] = 'nezávazně';
+$labels['mystatus'] = 'Můj stav';
+$labels['status'] = 'Stav';
+$labels['status-confirmed'] = 'Potvrzeno';
+$labels['status-cancelled'] = 'Zrušeno';
+$labels['priority'] = 'Přednost';
+$labels['sensitivity'] = 'Soukromí';
+$labels['public'] = 'veřejné';
+$labels['private'] = 'soukromé';
+$labels['confidential'] = 'důvěrné';
+$labels['links'] = 'Odkaz';
+$labels['alarms'] = 'Připomenutí';
+$labels['comment'] = 'Poznámka';
+$labels['created'] = 'Vytvořeno';
+$labels['changed'] = 'Naposledy změněno';
+$labels['unknown'] = 'Neznámý';
+$labels['eventoptions'] = 'Volby';
+$labels['generated'] = 'vytvořeno';
+$labels['eventhistory'] = 'Historie';
+$labels['removelink'] = 'Odstranit odkaz na e-mail';
+$labels['printdescriptions'] = 'Vytisknout popisy';
+$labels['parentcalendar'] = 'Vložit dovnitř';
+$labels['searchearlierdates'] = '« Hledat dřívější události';
+$labels['searchlaterdates'] = 'Hledat pozdější události »';
+$labels['andnmore'] = 'dalších $nr...';
+$labels['togglerole'] = 'Klepněte k přepnutí role';
+$labels['createfrommail'] = 'Uložit jako událost';
+$labels['importevents'] = 'Zavést události';
+$labels['importrange'] = 'Události od';
+$labels['onemonthback'] = '1 měsíc nazpátek';
+$labels['nmonthsback'] = '$nr měsíců nazpátek';
+$labels['showurl'] = 'Ukázat adresu (URL) kalendáře';
+$labels['showurldescription'] = 'Tuto adresu použijte pro přístup (jen ke čtení) ke kalendáři z jiných aplikací. Můžete ji zkopírovat a vložit do jakéhokoli kalendářového softwaru, který podporuje formát iCal.';
+$labels['caldavurldescription'] = 'Zkopírujte tuto adresu do aplikace <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> klienta (např. Evolution nebo Mozilla Thunderbird) pro úplné synchronizování tohoto adresáře s vaším počítačem nebo mobilním zařízením.';
+$labels['findcalendars'] = 'Najít kalendáře...';
+$labels['searchterms'] = 'Hledané výrazy';
+$labels['calsearchresults'] = 'Dostupné kalendáře';
+$labels['calendarsubscribe'] = 'Ukazovat seznam trvale';
+$labels['nocalendarsfound'] = 'Nenalezeny žádné kalendáře';
+$labels['nrcalendarsfound'] = '$nr kalendářů nalezeno';
+$labels['quickview'] = 'Zobrazit jen tento kalendář';
+$labels['invitationspending'] = 'Pozvání čekající na vyřízení';
+$labels['invitationsdeclined'] = 'Odmítnutá pozvání';
+$labels['changepartstat'] = 'Změnit stav příjemce';
+$labels['rsvpcomment'] = 'Text pozvánky';
+$labels['listrange'] = 'Rozsah k zobrazení:';
+$labels['listsections'] = 'Rozdělit na:';
+$labels['smartsections'] = 'Chytré sekce';
+$labels['until'] = 'do';
+$labels['today'] = 'Dnes';
+$labels['tomorrow'] = 'Zítra';
+$labels['thisweek'] = 'Tento týden';
+$labels['nextweek'] = 'Příští týden';
+$labels['prevweek'] = 'Předchozí týden';
+$labels['thismonth'] = 'Tento měsíc';
+$labels['nextmonth'] = 'Příští měsíc';
+$labels['weekofyear'] = 'Týden';
+$labels['pastevents'] = 'Minulost';
+$labels['futureevents'] = 'Budoucnost';
+$labels['showalarms'] = 'Ukázat připomenutí';
+$labels['defaultalarmtype'] = 'Výchozí nastavení připomenutí';
+$labels['defaultalarmoffset'] = 'Výchozí čas připomenutí';
+$labels['attendee'] = 'Účastník';
+$labels['role'] = 'Role';
+$labels['availability'] = 'Dost.';
+$labels['confirmstate'] = 'Stav';
+$labels['addattendee'] = 'Přidat účastníka';
+$labels['roleorganizer'] = 'Organizátor';
+$labels['rolerequired'] = 'Povinný';
+$labels['roleoptional'] = 'Nepovinný';
+$labels['rolechair'] = 'Předsednictví';
+$labels['rolenonparticipant'] = 'Nepřítomný';
+$labels['cutypeindividual'] = 'Jednotlivec';
+$labels['cutypegroup'] = 'Skupina';
+$labels['cutyperesource'] = 'Zdroj';
+$labels['cutyperoom'] = 'Místnost';
+$labels['availfree'] = 'Volno';
+$labels['availbusy'] = 'Obsazeno';
+$labels['availunknown'] = 'Neznámý';
+$labels['availtentative'] = 'Nezávazně';
+$labels['availoutofoffice'] = 'Mimo kancelář';
+$labels['delegatedto'] = 'Pověřený:';
+$labels['delegatedfrom'] = 'Pověřující:';
+$labels['scheduletime'] = 'Najít dostupnost';
+$labels['sendinvitations'] = 'Poslat pozvánky';
+$labels['sendnotifications'] = 'Uvědomit účastníky o změnách';
+$labels['sendcancellation'] = 'Uvědomit účastníky o zrušení události';
+$labels['onlyworkinghours'] = 'Najít dostupnost v mé pracovní době';
+$labels['reqallattendees'] = 'Povinní/všichni účastníci';
+$labels['prevslot'] = 'Předchozí místo v rozvrhu';
+$labels['nextslot'] = 'Další místo v rozvrhu';
+$labels['suggestedslot'] = 'Navržené místo v rozvrhu';
+$labels['noslotfound'] = 'Nelze najít volné místo v rozvrhu';
+$labels['invitationsubject'] = 'Byl(a) jste pozván(a) na událost "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nKdy: \$date\n\nPozváni: \$attendees\n\nPodrobnosti o události najdete v přiloženém souboru typu iCalendar. Můžete si ho zavést do kalendářového programu.";
+$labels['invitationattendlinks'] = "Pokud váš poštovní klient nepodporuje pozvánky iTip, použijte prosím následující odkaz k potvrzení nebo odmítnutí pozvání:\n\$url";
+$labels['eventupdatesubject'] = 'Událost "$title" byla aktualizována';
+$labels['eventupdatesubjectempty'] = 'Událost, která se vás týká, byla aktualizována';
+$labels['eventupdatemailbody'] = "*\$title*\n\nKdy: \$date\n\nPozváni: \$attendees\n\nPodrobnosti o aktualizované události najdete v přiloženém souboru typu iCalendar. Můžete si ho zavést do kalendářového programu.";
+$labels['eventcancelsubject'] = 'Událost "$title" byla zrušena';
+$labels['eventcancelmailbody'] = "*\$title*\n\nKdy: \$date\n\nPozváni: \$attendees\n\nUdálost byla zrušena organizátorem (\$organizer).\n\nPodrobnosti najdete v přiloženém souboru ve formátu iCalendar.";
+$labels['itipobjectnotfound'] = 'Událost, na který tato zpráva odkazuje, nebyl nalezen ve vašem kalendáři.';
+$labels['itipmailbodyaccepted'] = "\$sender přijal(a) pozvání na tuto událost:\n\n*\$title*\n\nKdy: \$date\n\nPozváni: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender nezávazně přijal(a) pozvání na tuto událost:\n\n*\$title*\n\nKdy: \$date\n\nPozváni: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender odmítl(a) pozvání na tuto událost:\n\n*\$title*\n\nKdy: \$date\n\nPozváni: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender odmítl vaši účast na následující události:\n\n*\$title*\n\nTermín: \$date";
+$labels['itipmailbodydelegated'] = "\$sender delegoval účast na následující události:\n\n*\$title*\n\nTermín: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender delegoval účast na následující události na vás:\n\n*\$title*\n\nTermín: \$date";
+$labels['itipdeclineevent'] = 'Opravdu chcete odmítnout pozvání na tuto událost?';
+$labels['declinedeleteconfirm'] = 'Chcete také ze svého kalendáře smazat tuto odmítnutou událost?';
+$labels['itipcomment'] = 'Poznámka k pozvání/oznámení';
+$labels['itipcommenttitle'] = 'Tato poznámka bude připojena ke zprávě s pozváním/oznámením poslané účastníkům';
+$labels['notanattendee'] = 'Nejste na seznamu účastníků této události';
+$labels['eventcancelled'] = 'Tato událost byla zrušena';
+$labels['saveincalendar'] = 'uložit do';
+$labels['updatemycopy'] = 'Aktualizovat v mém kalendáři';
+$labels['savetocalendar'] = 'Uložit do kalenáře';
+$labels['openpreview'] = 'Ověřit kalendář';
+$labels['noearlierevents'] = 'Žádné dřívější události';
+$labels['nolaterevents'] = 'Žádné pozdější události';
+$labels['resource'] = 'Zdroj';
+$labels['addresource'] = 'Zapsat zdroj';
+$labels['findresources'] = 'Najít zdroje';
+$labels['resourcedetails'] = 'Podrobnosti';
+$labels['resourceavailability'] = 'Dostupnost';
+$labels['resourceowner'] = 'Vlastník';
+$labels['resourceadded'] = 'Zdroj byl přidán do vaší události';
+$labels['tabsummary'] = 'Souhrn';
+$labels['tabrecurrence'] = 'Opakování';
+$labels['tabattendees'] = 'Účastníci';
+$labels['tabresources'] = 'Zdroje';
+$labels['tabattachments'] = 'Přílohy';
+$labels['tabsharing'] = 'Sdílení';
+$labels['deleteobjectconfirm'] = 'Opravdu chcete smazat tuto událost?';
+$labels['deleteventconfirm'] = 'Opravdu chcete smazat tuto událost?';
+$labels['deletecalendarconfirm'] = 'Opravdu chcete smazat tento kalendář se všemi událostmi?';
+$labels['deletecalendarconfirmrecursive'] = 'Do you really want to delete this calendar with all its events and sub-calendars?';
+$labels['savingdata'] = 'Ukládají se data...';
+$labels['errorsaving'] = 'Nelze uložit změny.';
+$labels['operationfailed'] = 'Požavovaná operace selhala.';
+$labels['invalideventdates'] = 'Vložená data nejsou platná! Zkontrolujte prosím zadávané údaje.';
+$labels['invalidcalendarproperties'] = 'Neplatné vlastnosti kalendáře! Vložte prosím platné jméno.';
+$labels['searchnoresults'] = 'Ve vybraných kalendářích nebyly nalezeny žádné události.';
+$labels['successremoval'] = 'Událost byla úspěšně smazána.';
+$labels['successrestore'] = 'Událost byla úspěšně obnovena.';
+$labels['errornotifying'] = 'Nelze odeslat notifikace účastníkům události';
+$labels['errorimportingevent'] = 'Událost se nepodařilo zavést';
+$labels['importwarningexists'] = 'Kopie této události již ve vašem kalendáři existuje.';
+$labels['newerversionexists'] = 'Existuje již novější verze této události! Operace byla zrušena.';
+$labels['nowritecalendarfound'] = 'Nebyl nalezen žádný kalendář, do kterého by šlo uložit tuto událost.';
+$labels['importedsuccessfully'] = 'Událost byla úspěšně přidána do kalendáře \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Událost byla úspěšně aktualizována v \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Stav účastníka byl úspěšně aktualizován';
+$labels['itipsendsuccess'] = 'Pozvánky byly rozeslány účastníkům.';
+$labels['itipresponseerror'] = 'Nelze odeslat odpověď na tuto pozvánku';
+$labels['itipinvalidrequest'] = 'Tato pozvánka již není platná';
+$labels['sentresponseto'] = 'Odpověď na pozvánku byla úspěšně odeslána na adresu $mailto';
+$labels['localchangeswarning'] = 'Chystáte se provést změny, které se projeví jen ve vašem vlastním kalendáři a nebudou poslány organizátorovi události.';
+$labels['importsuccess'] = 'Úspěšně zavedeno $nr událostí';
+$labels['importnone'] = 'Nebyly nalezeny žádné události k zavedení';
+$labels['importerror'] = 'Při zavádění došlo k chybě';
+$labels['aclnorights'] = 'Nemáte administrátorská práva k tomuto kalendáři.';
+$labels['changeeventconfirm'] = 'Změnit událost';
+$labels['removeeventconfirm'] = 'Smazat událost';
+$labels['changerecurringeventwarning'] = 'Toto je opakovaná událost. Chcete upravit jen toto konání, toto a všechna následující konání, úplně všechna konání nebo uložit událost jako novou?';
+$labels['removerecurringeventwarning'] = 'Toto je opakovaná událost. Chcete smazat jen toto konání, toto a všechna následující konání, nebo úplně všechna konání?';
+$labels['currentevent'] = 'Nynější';
+$labels['futurevents'] = 'Budoucí';
+$labels['allevents'] = 'Vše';
+$labels['saveasnew'] = 'Uložit jako novou';
+$labels['birthdays'] = 'Narozeniny';
+$labels['birthdayscalendar'] = 'Kalendář narozenin';
+$labels['displaybirthdayscalendar'] = 'Zobrazit kalendář narozenin';
+$labels['birthdayscalendarsources'] = 'Z těchto adresářů';
+$labels['birthdayeventtitle'] = 'Narozeniny $name';
+$labels['birthdayage'] = 'Věk $age';
+$labels['objectchangelog'] = 'Historie změn';
+$labels['revision'] = 'Verze';
+$labels['user'] = 'Uživatel';
+$labels['operation'] = 'Činnost';
+$labels['actionappend'] = 'Uloženo';
+$labels['actionmove'] = 'Přesunuto';
+$labels['actiondelete'] = 'Smazáno';
+$labels['compare'] = 'Porovnat';
+$labels['showrevision'] = 'Ukázat tuto verzi';
+$labels['restore'] = 'Obnovit tuto verzi';
+$labels['objectnotfound'] = 'Nepodařilo se nahrát data události';
+$labels['objectchangelognotavailable'] = 'Historie změn není pro tuto událost dostupná';
+$labels['objectdiffnotavailable'] = 'Pro vybrané verze není žádné srovnání možné';
+$labels['revisionrestoreconfirm'] = 'Opravdu chcete obnovit změnu $rev této události? Tímto dojde k nahrazení nynější události starou verzí.';
+$labels['arialabelminical'] = 'Výběr data v kalendáři';
+$labels['arialabelcalendarview'] = 'Pohled na kalendář';
+$labels['arialabelsearchform'] = 'Hledání události';
+$labels['arialabelquicksearchbox'] = 'Zadání hledání události';
+$labels['arialabelcalsearchform'] = 'Hledání kalendářů';
+$labels['calendaractions'] = 'Činnosti v kalendáři';
+$labels['arialabeleventattendees'] = 'Seznam účastníků události';
+$labels['arialabeleventresources'] = 'Seznam zdrojů události';
+$labels['arialabelresourcesearchform'] = 'Hledání zdrojů';
+$labels['arialabelresourceselection'] = 'Dostupné zdroje';
+?>
diff --git a/calendar/localization/da_DK.inc b/calendar/localization/da_DK.inc
new file mode 100644
index 0000000..e44851e
--- /dev/null
+++ b/calendar/localization/da_DK.inc
@@ -0,0 +1,274 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Standardvisning';
+$labels['time_format'] = 'Tidsformat';
+$labels['timeslots'] = 'Tidsblokke per time';
+$labels['first_day'] = 'Første ugedag';
+$labels['first_hour'] = 'Første time som vises';
+$labels['workinghours'] = 'Arbejdstider';
+$labels['add_category'] = 'Tilføj kategori';
+$labels['remove_category'] = 'Fjern kategori';
+$labels['defaultcalendar'] = 'Opret nye arragementer i';
+$labels['eventcoloring'] = 'Farver for arrangementer';
+$labels['coloringmode0'] = 'Ifølge kalender';
+$labels['coloringmode1'] = 'Ifølge kategori';
+$labels['coloringmode2'] = 'Kalender til oversigt, kategori til indhold';
+$labels['coloringmode3'] = 'Kategori til oversigt, kalender til indhold';
+$labels['afternothing'] = 'Undlad at gøre noget';
+$labels['aftertrash'] = 'Flyt til papirkurv';
+$labels['afterdelete'] = 'Slet beskeden';
+$labels['afterflagdeleted'] = 'Markér som slettet';
+$labels['aftermoveto'] = 'Flyt til ...';
+$labels['itipoptions'] = 'Begivenhedsinvitationer';
+$labels['afteraction'] = 'Efter en invitation eller opdateringsbesked er behandlet';
+$labels['calendar'] = 'Kalender';
+$labels['calendars'] = 'Kalendere';
+$labels['category'] = 'Kategori';
+$labels['categories'] = 'Kategorier';
+$labels['createcalendar'] = 'Opret ny kalender';
+$labels['editcalendar'] = 'Redigér kalenderegenskaber';
+$labels['name'] = 'Navn';
+$labels['color'] = 'Farve';
+$labels['day'] = 'Dag';
+$labels['week'] = 'Uge';
+$labels['month'] = 'Måned';
+$labels['agenda'] = 'Dagsorden';
+$labels['new'] = 'Ny';
+$labels['new_event'] = 'Nyt arrangement';
+$labels['edit_event'] = 'Redigér arrangement';
+$labels['edit'] = 'Redigér';
+$labels['save'] = 'Gem';
+$labels['removelist'] = 'Remove from list';
+$labels['cancel'] = 'Annullér';
+$labels['select'] = 'Vælg';
+$labels['print'] = 'Udskriv';
+$labels['printtitle'] = 'Udskriv kalendere';
+$labels['title'] = 'Resumé';
+$labels['description'] = 'Beskrivelse';
+$labels['all-day'] = 'hele-dagen';
+$labels['export'] = 'Eksport';
+$labels['exporttitle'] = 'Eksportér til iCalendar';
+$labels['exportrange'] = 'Arrangementer fra';
+$labels['exportattachments'] = 'Med vedhæftninger';
+$labels['customdate'] = 'Brugerdefineret dato';
+$labels['location'] = 'Placering';
+$labels['url'] = 'URL';
+$labels['date'] = 'Dato';
+$labels['start'] = 'Start';
+$labels['starttime'] = 'Start time';
+$labels['end'] = 'Slut';
+$labels['endtime'] = 'Sluttidspunkt';
+$labels['repeat'] = 'Gentag';
+$labels['selectdate'] = 'Vælg dato';
+$labels['freebusy'] = 'Vis mig som';
+$labels['free'] = 'Ledig';
+$labels['busy'] = 'Optaget';
+$labels['outofoffice'] = 'Ikke på kontoret';
+$labels['tentative'] = 'Forsøgsvis';
+$labels['mystatus'] = 'Min status';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Bekræftet';
+$labels['status-cancelled'] = 'Annulleret';
+$labels['priority'] = 'Prioritet';
+$labels['sensitivity'] = 'Privatliv';
+$labels['public'] = 'offentlig';
+$labels['private'] = 'privat';
+$labels['confidential'] = 'fortrolig';
+$labels['links'] = 'Reference';
+$labels['alarms'] = 'Påmindelse';
+$labels['comment'] = 'Comment';
+$labels['created'] = 'Created';
+$labels['changed'] = 'Last Modified';
+$labels['unknown'] = 'Ukendt';
+$labels['eventoptions'] = 'Tilvalg';
+$labels['generated'] = 'oprettet per';
+$labels['eventhistory'] = 'Historik';
+$labels['removelink'] = 'Remove email reference';
+$labels['printdescriptions'] = 'Udskriv beskrivelser';
+$labels['parentcalendar'] = 'Indsæt indeni';
+$labels['searchearlierdates'] = '« Søg efter tidligere arrangementer';
+$labels['searchlaterdates'] = 'Søg efter senere arrangementer »';
+$labels['andnmore'] = '$nr flere...';
+$labels['togglerole'] = 'Klik for at vise eller skjule rolle';
+$labels['createfrommail'] = 'Gem som arrangement';
+$labels['importevents'] = 'Importér arrangement';
+$labels['importrange'] = 'Arrangementer fra';
+$labels['onemonthback'] = '1 måned tilbage';
+$labels['nmonthsback'] = '$nr måneder tilbage';
+$labels['showurl'] = 'Vis kalenderens URL';
+$labels['showurldescription'] = 'Brug følgende adresse for at tilgå din kalender (skrivebeskyttet) fra andre programmer. Du kan kopiere og indsætet denne i ethvert kalenderprogram, der understøtter iCal-formatet.';
+$labels['caldavurldescription'] = 'Kopiér denne adresse til en <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a>-klientprogram (eks. Evolution eller Mozilla Thunderbird) for at synkronisere denne kalender komplet med din computer eller mobilenhed.';
+$labels['findcalendars'] = 'Find kalendere ...';
+$labels['searchterms'] = 'Search terms';
+$labels['calsearchresults'] = 'Tilgængelige kalendere';
+$labels['calendarsubscribe'] = 'List permanently';
+$labels['nocalendarsfound'] = 'Der blev ikke fundet nogen kalender';
+$labels['nrcalendarsfound'] = '$nr kalendere blev fundet';
+$labels['quickview'] = 'Vis kun denne kalender';
+$labels['invitationspending'] = 'Afventende invitationer';
+$labels['invitationsdeclined'] = 'Afviste invitationer';
+$labels['changepartstat'] = 'Skift deltagerstatus';
+$labels['rsvpcomment'] = 'Invitationstekst';
+$labels['listrange'] = 'Interval som skal vises:';
+$labels['listsections'] = 'Del op i:';
+$labels['smartsections'] = 'Smarte sektioner';
+$labels['until'] = 'indtil';
+$labels['today'] = 'I dag';
+$labels['tomorrow'] = 'I morgen';
+$labels['thisweek'] = 'Denne uge';
+$labels['nextweek'] = 'Næste uge';
+$labels['prevweek'] = 'Forrige uge';
+$labels['thismonth'] = 'Denne måned';
+$labels['nextmonth'] = 'Næste måned';
+$labels['weekofyear'] = 'Uge';
+$labels['pastevents'] = 'Tidligere';
+$labels['futureevents'] = 'Fremtid';
+$labels['showalarms'] = 'Show reminders';
+$labels['defaultalarmtype'] = 'Standardindstilling for påmindelse';
+$labels['defaultalarmoffset'] = 'Standardtidspunkt for påmindelse';
+$labels['attendee'] = 'Deltager';
+$labels['role'] = 'Rolle';
+$labels['availability'] = 'Tilg.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Tilføj deltager';
+$labels['roleorganizer'] = 'Organisator';
+$labels['rolerequired'] = 'Påkrævet';
+$labels['roleoptional'] = 'Valgfri';
+$labels['rolechair'] = 'Formand';
+$labels['rolenonparticipant'] = 'Fraværende';
+$labels['cutypeindividual'] = 'Individuel';
+$labels['cutypegroup'] = 'Gruppe';
+$labels['cutyperesource'] = 'Ressource';
+$labels['cutyperoom'] = 'Lokale';
+$labels['availfree'] = 'Ledig';
+$labels['availbusy'] = 'Optaget';
+$labels['availunknown'] = 'Ukendt';
+$labels['availtentative'] = 'Forsøgsvis';
+$labels['availoutofoffice'] = 'Ikke på kontoret';
+$labels['delegatedto'] = 'Delegated to: ';
+$labels['delegatedfrom'] = 'Delegated from: ';
+$labels['scheduletime'] = 'Find ledigt tidspunkt';
+$labels['sendinvitations'] = 'Send invitationer';
+$labels['sendnotifications'] = 'Gør deltagere opmærksom på ændringer';
+$labels['sendcancellation'] = 'Giv deltagere besked om aflysning af arrangementer';
+$labels['onlyworkinghours'] = 'Find ledigt tidspunkt inden for mine arbejdstider';
+$labels['reqallattendees'] = 'Påkrævet/alle deltagere';
+$labels['prevslot'] = 'Forrige blok';
+$labels['nextslot'] = 'Næste blok';
+$labels['suggestedslot'] = 'Foreslået blok';
+$labels['noslotfound'] = 'Kunne ikke finde en ledig tidsblok';
+$labels['invitationsubject'] = 'Du er blevet inviteret til "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nTidspunkt: \$date\n\nInviterede: \$attendees\n\nBemærk venligst vedhæftede iCalendar-fil med alle detaljer om arrangementet, som du kan importere til dit kalenderprogram.";
+$labels['invitationattendlinks'] = "Hvis dit e-postprogram ikke understøtter iTip-forespørgsler, så kan du benytte følgende henvisning til enten at acceptere eller afvise denne invitation:\n\$url";
+$labels['eventupdatesubject'] = '"$title" er blevet opdateret';
+$labels['eventupdatesubjectempty'] = 'Et arrangement der vedrører dig er blevet opdateret';
+$labels['eventupdatemailbody'] = "*\$title*\n\nTidspunkt: \$date\n\nInviterede: \$attendees\n\nBemærk venligst vedhæftede iCalendar-fil med alle detaljer om arrangementet, som du kan importere til dit kalenderprogram.";
+$labels['eventcancelsubject'] = '"$title" er blevet aflyst';
+$labels['eventcancelmailbody'] = "*\$title*\n\nTidspunkt: \$date\n\nInviterede: \$attendees\n\nDette arrangement er blevet aflyst af \$organizer.\n\nBemærk venligst vedhæftede iCalendard-fil med de opdaterede detaljer om arrangementet.";
+$labels['itipobjectnotfound'] = 'Begivenheden som denne besked henviser til, blev ikke fundet i din kalender.';
+$labels['itipmailbodyaccepted'] = "\$sender har accepteret invitationen til det følgende arrangement:\n\n*\$title*\n\nTidspunkt: \$date\n\nInviterede: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender har forsøgsvist accepteret invitationen til det følgende arrangement:\n\n*\$title*\n\nTidspunkt: \$date\n\nInviterede: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender har afvist invitationen til det følgende arrangement:\n\n*\$title*\n\nTidspunkt: \$date\n\nInviterede: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender har afvist din deltagelse i følgende begivenhed:\n\n*\$title*\n\nTidspunkt: \$date";
+$labels['itipmailbodydelegated'] = "\$sender har delegeret deltagelsen i følgende begivenhed:\n\n*\$title*\n\nTidspunkt: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender har delegeret deltagelsen i følgende begivenhed til dig:\n\n*\$title*\n\nTidspunkt: \$date";
+$labels['itipdeclineevent'] = 'Sikker på at du vil afvise dette arrangement?';
+$labels['declinedeleteconfirm'] = 'Vil du også slette dette afviste arrangement fra din kalender?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['itipcommenttitle'] = 'Denne kommentar vil blive føjet til den besked med invitation/notifikation, der sendes til deltagerne';
+$labels['notanattendee'] = 'Du er ikke opført som deltager for dette arrangement';
+$labels['eventcancelled'] = 'Arrangementet er blevet aflyst';
+$labels['saveincalendar'] = 'gem i';
+$labels['updatemycopy'] = 'Opdatér i min kalender';
+$labels['savetocalendar'] = 'Gem til kalender';
+$labels['openpreview'] = 'Tjek kalender';
+$labels['noearlierevents'] = 'Ingen tidligere begivenheder';
+$labels['nolaterevents'] = 'Ingen senere begivenheder';
+$labels['resource'] = 'Ressource';
+$labels['addresource'] = 'Booking af ressource';
+$labels['findresources'] = 'Find ressourcer';
+$labels['resourcedetails'] = 'Detaljer';
+$labels['resourceavailability'] = 'Tilgængelighed';
+$labels['resourceowner'] = 'Ejer';
+$labels['resourceadded'] = 'Ressourcen til føjet til din begivenhed';
+$labels['tabsummary'] = 'Resumé';
+$labels['tabrecurrence'] = 'Gentagelse';
+$labels['tabattendees'] = 'Deltagere';
+$labels['tabresources'] = 'Ressourcer';
+$labels['tabattachments'] = 'Vedhæftninger';
+$labels['tabsharing'] = 'Deling';
+$labels['deleteobjectconfirm'] = 'Sikker på at du vil slette dette arrangement?';
+$labels['deleteventconfirm'] = 'Sikker på at du vil slette dette arrangement?';
+$labels['deletecalendarconfirm'] = 'Sikker på at du vil slette denne kalender med alle dets arrangementer?';
+$labels['deletecalendarconfirmrecursive'] = 'Sikker på du vil slette denne kalender med alle dens arrangementer og delkalendere?';
+$labels['savingdata'] = 'Gemmer data...';
+$labels['errorsaving'] = 'Kunne ikke gemme ændringer.';
+$labels['operationfailed'] = 'Den forespurgte handling mislykkedes.';
+$labels['invalideventdates'] = 'Ugyldig dato indtastet! Tjek venligst dit input.';
+$labels['invalidcalendarproperties'] = 'Ugyldige kalenderegenskaber! Angiv venligst et gyldigt navn.';
+$labels['searchnoresults'] = 'Der blev ikke fundet arrangementer i de valgte kalendere.';
+$labels['successremoval'] = 'Sletning af arrangementet blev gennemført.';
+$labels['successrestore'] = 'Gendannelse af arrangementet blev gennemført.';
+$labels['errornotifying'] = 'Kunne ikke sende notifikation til arrangementets deltagere';
+$labels['errorimportingevent'] = 'Kunne ikke importere arrangementet';
+$labels['importwarningexists'] = 'En kopi af denne begivenhed findes allerede i din kalender.';
+$labels['newerversionexists'] = 'Der findes allerede en nyere version af arrangementet! Afbrød.';
+$labels['nowritecalendarfound'] = 'Ingen funden kalender til lagring af arrangementet';
+$labels['importedsuccessfully'] = 'Arrangementet blev tilføjet til \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Opdatering af begivenheden blev gennemført i "$calendar"';
+$labels['attendeupdateesuccess'] = 'Opdatering af deltagernes status blev gennemført';
+$labels['itipsendsuccess'] = 'Invitation blev sendt til deltagerne.';
+$labels['itipresponseerror'] = 'Kunne ikke sende svar til denne arrangementsinvitation';
+$labels['itipinvalidrequest'] = 'Denne invitation er ikke længere gyldig';
+$labels['sentresponseto'] = 'Gennemførte afsendelse af invitationssvar til $mailto';
+$labels['localchangeswarning'] = 'Du er i færd med at foretage ændringer, der vil påvirke din kalender og som ikke vil blive sendt til afholderen af arrangementet.';
+$labels['importsuccess'] = 'Gennemførte import af $nr arrangementer';
+$labels['importnone'] = 'Fandt ingen arrangementer som kunne importeres';
+$labels['importerror'] = 'Der opstod en fejl under import';
+$labels['aclnorights'] = 'Du har ikke administratorrettigheder for denne kalender.';
+$labels['changeeventconfirm'] = 'Tilpas arrangement';
+$labels['removeeventconfirm'] = 'Slet begivenhed';
+$labels['changerecurringeventwarning'] = 'Dette er et tilbagevendende arrangement. Ønsker du kun at redige det aktuelle arrangement, dette og alle fremtidige forekomster, alle forekomster eller gemme det som et nyt arrangement?';
+$labels['removerecurringeventwarning'] = 'Dette er en tilbagevendende begivenhed. Ønsker du kun at fjerne den aktuelle begivenhed, denne og alle fremtidige forekomster for denne begivenhed?';
+$labels['removerecurringallonly'] = 'Dette er en tilbagevendende begivenhed. Som deltager kan du slette kun slette hele begivenheden med alle dens forekomster.';
+$labels['currentevent'] = 'Nuværende';
+$labels['futurevents'] = 'Fremtid';
+$labels['allevents'] = 'Alle';
+$labels['saveasnew'] = 'Gem som ny';
+$labels['birthdays'] = 'Fødselsdage';
+$labels['birthdayscalendar'] = 'Fødselsdagskalender';
+$labels['displaybirthdayscalendar'] = 'Vis fødselsdagskalender';
+$labels['birthdayscalendarsources'] = 'Fra disse adressebøger';
+$labels['birthdayeventtitle'] = '$name har fødselsdag';
+$labels['birthdayage'] = '$age år';
+$labels['objectchangelog'] = 'Ændringshistorik';
+$labels['revision'] = 'Revision';
+$labels['user'] = 'Bruger';
+$labels['operation'] = 'Handling';
+$labels['actionappend'] = 'Gemt';
+$labels['actionmove'] = 'Flyttet';
+$labels['actiondelete'] = 'Slettet';
+$labels['compare'] = 'Sammenlign';
+$labels['showrevision'] = 'Vis denne version';
+$labels['restore'] = 'Genskab denne version';
+$labels['objectnotfound'] = 'Kunne ikke indlæse begivenhedsdata';
+$labels['objectchangelognotavailable'] = 'Ændringshistorikken er ikke tilgængelig for denne begivenhed';
+$labels['objectdiffnotavailable'] = 'Det er ikke muligt at sammenligne de valgte revisioner';
+$labels['revisionrestoreconfirm'] = 'Sikker på at du vil genskabe revision $rev af denne begivenhed? Dette vil erstatte den nuværende begivenhed med den tidligere version.';
+$labels['arialabelminical'] = 'Valg af kalenderdato';
+$labels['arialabelcalendarview'] = 'Kalendervisning';
+$labels['arialabelsearchform'] = 'Søgeformular for begivenheder';
+$labels['arialabelquicksearchbox'] = 'Søgeinput for begivenhed';
+$labels['arialabelcalsearchform'] = 'Søgeformular for kalendere';
+$labels['calendaractions'] = 'Kalenderhandlinger';
+$labels['arialabeleventattendees'] = 'Deltagerliste for begivenhed';
+$labels['arialabeleventresources'] = 'Ressourceliste for begivenhed';
+$labels['arialabelresourcesearchform'] = 'Søgeformular for ressourcer';
+$labels['arialabelresourceselection'] = 'Tilgængelige ressourcer';
+?>
diff --git a/calendar/localization/de_CH.inc b/calendar/localization/de_CH.inc
new file mode 100644
index 0000000..91726ca
--- /dev/null
+++ b/calendar/localization/de_CH.inc
@@ -0,0 +1,184 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Ansicht';
+$labels['time_format'] = 'Zeitformatierung';
+$labels['first_day'] = 'Erster Wochentag';
+$labels['first_hour'] = 'Erste angezeigte Stunde';
+$labels['workinghours'] = 'Arbeitszeiten';
+$labels['add_category'] = 'Kategorie hinzufügen';
+$labels['remove_category'] = 'Kategorie entfernen';
+$labels['defaultcalendar'] = 'Neue Termine erstellen in';
+$labels['eventcoloring'] = 'Färbung der Termine';
+$labels['coloringmode0'] = 'Farbe des Kalenders';
+$labels['coloringmode1'] = 'Farbe der Kategorie';
+$labels['coloringmode2'] = 'Kalenderfarbe aussen, Kategoriefarbe innen';
+$labels['coloringmode3'] = 'Kategoriefarbe aussen, Kalenderfarbe innen';
+$labels['calendar'] = 'Kalender';
+$labels['calendars'] = 'Kalender';
+$labels['category'] = 'Kategorie';
+$labels['categories'] = 'Kategorien';
+$labels['createcalendar'] = 'Neuen Kalender erstellen';
+$labels['editcalendar'] = 'Kalendereigenschaften bearbeiten';
+$labels['name'] = 'Name';
+$labels['color'] = 'Farbe';
+$labels['day'] = 'Tag';
+$labels['week'] = 'Woche';
+$labels['month'] = 'Monat';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Neu';
+$labels['new_event'] = 'Neuer Termin';
+$labels['edit_event'] = 'Termin bearbeiten';
+$labels['edit'] = 'Bearbeiten';
+$labels['save'] = 'Speichern';
+$labels['cancel'] = 'Abbrechen';
+$labels['select'] = 'Auswählen';
+$labels['print'] = 'Drucken';
+$labels['printtitle'] = 'Kalender drucken';
+$labels['title'] = 'Titel';
+$labels['description'] = 'Beschrieb';
+$labels['all-day'] = 'ganztägig';
+$labels['export'] = 'Exportieren';
+$labels['exporttitle'] = 'Kalender als iCalendar exportieren';
+$labels['exportrange'] = 'Termine ab';
+$labels['location'] = 'Ort';
+$labels['url'] = 'URL';
+$labels['date'] = 'Datum';
+$labels['start'] = 'Beginn';
+$labels['end'] = 'Ende';
+$labels['repeat'] = 'Wiederholung';
+$labels['selectdate'] = 'Datum auswählen';
+$labels['freebusy'] = 'Zeige mich als';
+$labels['free'] = 'Frei';
+$labels['busy'] = 'Gebucht';
+$labels['outofoffice'] = 'Abwesend';
+$labels['tentative'] = 'Mit Vorbehalt';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Bestätigt';
+$labels['status-cancelled'] = 'Gekündigt';
+$labels['priority'] = 'Priorität';
+$labels['sensitivity'] = 'Sichtbarkeit';
+$labels['public'] = 'öffentlich';
+$labels['private'] = 'privat';
+$labels['confidential'] = 'vertraulich';
+$labels['alarms'] = 'Erinnerung';
+$labels['comment'] = 'Kommentar';
+$labels['unknown'] = 'Unbekannt';
+$labels['generated'] = 'erstellt am';
+$labels['printdescriptions'] = 'Beschrieb drucken';
+$labels['parentcalendar'] = 'Erstellen in';
+$labels['searchearlierdates'] = '« Frühere Termine suchen';
+$labels['searchlaterdates'] = 'Spätere Termine suchen »';
+$labels['andnmore'] = '$nr weitere...';
+$labels['togglerole'] = 'Zum Ändern der Rolle klicken';
+$labels['createfrommail'] = 'Als Termin speichern';
+$labels['importevents'] = 'Termine importieren';
+$labels['importrange'] = 'Termine ab';
+$labels['onemonthback'] = '1 Monat zurück';
+$labels['nmonthsback'] = '$nr Monate zurück';
+$labels['showurl'] = 'URL anzeigen';
+$labels['showurldescription'] = 'Über die folgende Adresse können Sie mit einem beliebigen Kalenderprogramm Ihren Kalender abrufen (nur lesend), sofern dieses das iCal-Format unterstützt.';
+$labels['listrange'] = 'Angezeigter Bereich:';
+$labels['listsections'] = 'Unterteilung:';
+$labels['smartsections'] = 'Intelligent';
+$labels['until'] = 'bis';
+$labels['today'] = 'Heute';
+$labels['tomorrow'] = 'Morgen';
+$labels['thisweek'] = 'Diese Woche';
+$labels['nextweek'] = 'Nächste Woche';
+$labels['thismonth'] = 'Diesen Monat';
+$labels['nextmonth'] = 'Nächsten Monat';
+$labels['weekofyear'] = 'KW';
+$labels['pastevents'] = 'Vergangene';
+$labels['futureevents'] = 'Zukünftige';
+$labels['defaultalarmtype'] = 'Standard-Erinnerungseinstellung';
+$labels['defaultalarmoffset'] = 'Standard-Erinnerungszeit';
+$labels['attendee'] = 'Teilnehmer';
+$labels['role'] = 'Rolle';
+$labels['availability'] = 'Verfüg.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Hinzufügen';
+$labels['roleorganizer'] = 'Organisator';
+$labels['rolerequired'] = 'Erforderlich';
+$labels['roleoptional'] = 'Optional';
+$labels['availfree'] = 'Frei';
+$labels['availbusy'] = 'Gebucht';
+$labels['availunknown'] = 'Unbekannt';
+$labels['availtentative'] = 'Mit Vorbehalt';
+$labels['availoutofoffice'] = 'Abwesend';
+$labels['scheduletime'] = 'Verfügbarkeit anzeigen';
+$labels['sendinvitations'] = 'Einladungen versenden';
+$labels['sendnotifications'] = 'Teilnehmer über die Änderungen informieren';
+$labels['sendcancellation'] = 'Teilnehmer über die Terminabsage informieren';
+$labels['onlyworkinghours'] = 'Verfügbarkeit innerhalb meiner Arbeitszeiten suchen';
+$labels['reqallattendees'] = 'Erforderliche/alle Teilnehmer';
+$labels['prevslot'] = 'Vorheriger Vorschlag';
+$labels['nextslot'] = 'Nächster Vorschlag';
+$labels['noslotfound'] = 'Es konnten keine freien Zeiten gefunden werden';
+$labels['invitationsubject'] = 'Sie wurden zu "$title" eingeladen';
+$labels['invitationmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit allen Details des Termins. Diese können Sie in Ihre Kalenderanwendung importieren.";
+$labels['invitationattendlinks'] = "Falls Ihr E-Mail-Programm keine iTip-Anfragen unterstützt, können Sie den folgenden Link verwenden, um den Termin zu bestätigen oder abzulehnen:\n\$url";
+$labels['eventupdatesubject'] = '"$title" wurde aktualisiert';
+$labels['eventupdatesubjectempty'] = 'Termin wurde aktualisiert';
+$labels['eventupdatemailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit den aktualisiereten Termindaten. Diese können Sie in Ihre Kalenderanwendung importieren.";
+$labels['eventcancelsubject'] = '"$title" wurde abgesagt';
+$labels['eventcancelmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nDer Termin wurde von \$organizer abgesagt.\n\nIm Anhang finden Sie eine iCalendar-Datei mit den Termindaten.";
+$labels['itipmailbodyaccepted'] = "\$sender hat die Einladung zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender hat die Einladung mit Vorbehalt zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender hat die Einladung zum folgenden Termin abgelehnt:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
+$labels['itipdeclineevent'] = 'Möchten Sie die Einladung zu diesem Termin ablehnen?';
+$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
+$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgeführt';
+$labels['eventcancelled'] = 'Der Termin wurde vom Organisator abgesagt';
+$labels['saveincalendar'] = 'speichern in';
+$labels['resource'] = 'Ressource';
+$labels['resourcedetails'] = 'Details';
+$labels['tabsummary'] = 'Übersicht';
+$labels['tabrecurrence'] = 'Wiederholung';
+$labels['tabattendees'] = 'Teilnehmer';
+$labels['tabattachments'] = 'Anhänge';
+$labels['tabsharing'] = 'Freigabe';
+$labels['deleteobjectconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
+$labels['deleteventconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
+$labels['deletecalendarconfirm'] = 'Möchten Sie diesen Kalender mit allen Terminen wirklich löschen?';
+$labels['savingdata'] = 'Speichere Daten...';
+$labels['errorsaving'] = 'Fehler beim Speichern.';
+$labels['operationfailed'] = 'Die Aktion ist fehlgeschlagen.';
+$labels['invalideventdates'] = 'Ungültige Daten eingegeben! Bitte überprüfen Sie die Eingaben.';
+$labels['invalidcalendarproperties'] = 'Ungültige Kalenderinformationen! Bitte geben Sie einen Namen ein.';
+$labels['searchnoresults'] = 'Keine Termine in den gewählten Kalendern gefunden.';
+$labels['successremoval'] = 'Der Termin wurde erfolgreich gelöscht.';
+$labels['successrestore'] = 'Der Termin wurde erfolgreich wieder hergestellt.';
+$labels['errornotifying'] = 'Benachrichtigung an die Teilnehmer konnten nicht gesendet werden';
+$labels['errorimportingevent'] = 'Fehler beim Importieren';
+$labels['newerversionexists'] = 'Eine neuere Version dieses Termins exisitert bereits! Import abgebrochen.';
+$labels['nowritecalendarfound'] = 'Kein Kalender zum Speichern gefunden';
+$labels['importedsuccessfully'] = 'Der Termin wurde erfolgreich in \'$calendar\' gespeichert';
+$labels['attendeupdateesuccess'] = 'Teilnehmerstatus erfolgreich aktualisiert';
+$labels['itipsendsuccess'] = 'Einladung an Teilnehmer versendet.';
+$labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden';
+$labels['itipinvalidrequest'] = 'Diese Einladung ist nicht mehr gültig';
+$labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet';
+$labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert';
+$labels['importnone'] = 'Keine Termine zum Importieren gefunden';
+$labels['importerror'] = 'Fehler beim Importieren';
+$labels['aclnorights'] = 'Sie haben keine Administrator-Rechte für diesen Kalender.';
+$labels['changeeventconfirm'] = 'Termin ändern';
+$labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';
+$labels['currentevent'] = 'Aktuellen';
+$labels['futurevents'] = 'Zukünftige';
+$labels['allevents'] = 'Alle';
+$labels['saveasnew'] = 'Als neu speichern';
+$labels['birthdays'] = 'Geburtstage';
+$labels['birthdayscalendar'] = 'Geburtstags-Kalender';
+$labels['displaybirthdayscalendar'] = 'Geburtstags-Kalender anzeigen';
+$labels['birthdayscalendarsources'] = 'Für diese Adressbücher';
+$labels['birthdayeventtitle'] = '$names Geburtstag';
+$labels['birthdayage'] = 'Alter $age';
+$labels['actiondelete'] = 'Gelöscht';
+?>
diff --git a/calendar/localization/de_DE.inc b/calendar/localization/de_DE.inc
new file mode 100644
index 0000000..53de5b5
--- /dev/null
+++ b/calendar/localization/de_DE.inc
@@ -0,0 +1,284 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Standardansicht';
+$labels['time_format'] = 'Zeitformatierung';
+$labels['timeslots'] = 'Time slots per hour';
+$labels['first_day'] = 'Erster Wochentag';
+$labels['first_hour'] = 'Erste angezeigte Stunde';
+$labels['workinghours'] = 'Arbeitszeiten';
+$labels['add_category'] = 'Kategorie hinzufügen';
+$labels['remove_category'] = 'Kategorie entfernen';
+$labels['defaultcalendar'] = 'Neue Termine erstellen in';
+$labels['eventcoloring'] = 'Färbung der Termine';
+$labels['coloringmode0'] = 'Farbe des Kalenders';
+$labels['coloringmode1'] = 'Farbe der Kategorie';
+$labels['coloringmode2'] = 'Kalenderfarbe außen, Kategoriefarbe innen';
+$labels['coloringmode3'] = 'Kategoriefarbe außen, Kalenderfarbe innen';
+$labels['afternothing'] = 'nichts unternehmen';
+$labels['aftertrash'] = 'In den Papierkorb verschieben';
+$labels['afterdelete'] = 'Nachricht löschen';
+$labels['afterflagdeleted'] = 'Als gelöscht markieren';
+$labels['aftermoveto'] = 'Verschiebe nach...';
+$labels['itipoptions'] = 'Veranstaltungseinladungen';
+$labels['afteraction'] = 'Nachdem eine Einladungs- oder Update-Nachricht verarbetet wurde';
+$labels['calendar'] = 'Kalender';
+$labels['calendars'] = 'Kalender';
+$labels['category'] = 'Kategorie';
+$labels['categories'] = 'Kategorien';
+$labels['createcalendar'] = 'Neuen Kalender erstellen';
+$labels['editcalendar'] = 'Kalendereigenschaften bearbeiten';
+$labels['name'] = 'Name';
+$labels['color'] = 'Farbe';
+$labels['day'] = 'Tag';
+$labels['week'] = 'Woche';
+$labels['month'] = 'Monat';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Neu';
+$labels['new_event'] = 'Neuer Termin';
+$labels['edit_event'] = 'Termin bearbeiten';
+$labels['edit'] = 'Bearbeiten';
+$labels['save'] = 'Speichern';
+$labels['removelist'] = 'Von der Liste entfernen';
+$labels['cancel'] = 'Abbrechen';
+$labels['select'] = 'Auswählen';
+$labels['print'] = 'Drucken';
+$labels['printtitle'] = 'Kalender drucken';
+$labels['title'] = 'Titel';
+$labels['description'] = 'Beschreibung';
+$labels['all-day'] = 'ganztägig';
+$labels['export'] = 'Exportieren';
+$labels['exporttitle'] = 'Kalender als iCalendar exportieren';
+$labels['exportrange'] = 'Termine ab';
+$labels['exportattachments'] = 'Mit Anhängen';
+$labels['customdate'] = 'Benutzerdefiniertes Datum';
+$labels['location'] = 'Ort';
+$labels['url'] = 'URL';
+$labels['date'] = 'Datum';
+$labels['start'] = 'Beginn';
+$labels['starttime'] = 'Startzeit';
+$labels['end'] = 'Ende';
+$labels['endtime'] = 'Endzeit';
+$labels['repeat'] = 'Wiederholung';
+$labels['selectdate'] = 'Datum auswählen';
+$labels['freebusy'] = 'Zeige mich als';
+$labels['free'] = 'Frei';
+$labels['busy'] = 'Gebucht';
+$labels['outofoffice'] = 'Abwesend';
+$labels['tentative'] = 'Mit Vorbehalt';
+$labels['mystatus'] = 'Mein Status';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Bestätigt';
+$labels['status-cancelled'] = 'Abgesagt';
+$labels['priority'] = 'Priorität';
+$labels['sensitivity'] = 'Sichtbarkeit';
+$labels['public'] = 'öffentlich';
+$labels['private'] = 'privat';
+$labels['confidential'] = 'vertraulich';
+$labels['links'] = 'Referenz';
+$labels['alarms'] = 'Erinnerung';
+$labels['comment'] = 'Kommentar';
+$labels['created'] = 'Erstellt';
+$labels['changed'] = 'Geändert';
+$labels['unknown'] = 'Unbekannt';
+$labels['eventoptions'] = 'Optionen';
+$labels['generated'] = 'erstellt am';
+$labels['eventhistory'] = 'Historie';
+$labels['removelink'] = 'E-Mail-Referenz entfernen';
+$labels['printdescriptions'] = 'Beschreibung drucken';
+$labels['parentcalendar'] = 'Erstellen in';
+$labels['searchearlierdates'] = '« Frühere Termine suchen';
+$labels['searchlaterdates'] = 'Spätere Termine suchen »';
+$labels['andnmore'] = '$nr weitere...';
+$labels['togglerole'] = 'Zum Ändern der Rolle klicken';
+$labels['createfrommail'] = 'Als Termin speichern';
+$labels['importevents'] = 'Termine importieren';
+$labels['importrange'] = 'Termine ab';
+$labels['onemonthback'] = '1 Monat zurück';
+$labels['nmonthsback'] = '$nr Monate zurück';
+$labels['showurl'] = 'URL anzeigen';
+$labels['showurldescription'] = 'Über die folgende Adresse können Sie mit einem beliebigen Kalenderprogramm Ihren Kalender abrufen (nur lesend), sofern dieses das iCal-Format unterstützt.';
+$labels['caldavurldescription'] = 'Diese Adresse in einen <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a>-Klienten (z.B. Evolution oder Mozilla Thunderbird) kopieren, um den Kalender in Gänze mit einem mobilen Gerät zu synchronisieren.';
+$labels['findcalendars'] = 'Kalender finden...';
+$labels['searchterms'] = 'Suchbegriffe';
+$labels['calsearchresults'] = 'Verfügbare Kalender';
+$labels['calendarsubscribe'] = 'Permanent anzeigen';
+$labels['nocalendarsfound'] = 'Keine Kalender gefunden';
+$labels['nrcalendarsfound'] = '$nr Kalender gefunden';
+$labels['quickview'] = 'Nur diesen Kalender anzeigen';
+$labels['invitationspending'] = 'Ausstehende Einladungen';
+$labels['invitationsdeclined'] = 'Abgelehnte Einladungen';
+$labels['changepartstat'] = 'Teilnehmerstatus ändern';
+$labels['rsvpcomment'] = 'Einladungstext';
+$labels['listrange'] = 'Angezeigter Bereich:';
+$labels['listsections'] = 'Unterteilung:';
+$labels['smartsections'] = 'Intelligent';
+$labels['until'] = 'bis';
+$labels['today'] = 'Heute';
+$labels['tomorrow'] = 'Morgen';
+$labels['thisweek'] = 'Diese Woche';
+$labels['nextweek'] = 'Nächste Woche';
+$labels['prevweek'] = 'Vorige Woche';
+$labels['thismonth'] = 'Diesen Monat';
+$labels['nextmonth'] = 'Nächsten Monat';
+$labels['weekofyear'] = 'Woche';
+$labels['pastevents'] = 'Vergangene';
+$labels['futureevents'] = 'Zukünftige';
+$labels['showalarms'] = 'Show reminders';
+$labels['defaultalarmtype'] = 'Standard-Erinnerungseinstellung';
+$labels['defaultalarmoffset'] = 'Standard-Erinnerungszeit';
+$labels['attendee'] = 'Teilnehmer';
+$labels['role'] = 'Rolle';
+$labels['availability'] = 'Verfüg.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Hinzufügen';
+$labels['roleorganizer'] = 'Organisator';
+$labels['rolerequired'] = 'Erforderlich';
+$labels['roleoptional'] = 'Optional';
+$labels['rolechair'] = 'Vorsitz';
+$labels['rolenonparticipant'] = 'Absent';
+$labels['cutypeindividual'] = 'Person';
+$labels['cutypegroup'] = 'Gruppe';
+$labels['cutyperesource'] = 'Ressource';
+$labels['cutyperoom'] = 'Raum';
+$labels['availfree'] = 'Frei';
+$labels['availbusy'] = 'Gebucht';
+$labels['availunknown'] = 'Unbekannt';
+$labels['availtentative'] = 'Mit Vorbehalt';
+$labels['availoutofoffice'] = 'Abwesend';
+$labels['delegatedto'] = 'Delegiert an:';
+$labels['delegatedfrom'] = 'Delegiert von:';
+$labels['scheduletime'] = 'Verfügbarkeit anzeigen';
+$labels['sendinvitations'] = 'Einladungen versenden';
+$labels['sendnotifications'] = 'Teilnehmer über die Änderungen informieren';
+$labels['sendcancellation'] = 'Teilnehmer über die Terminabsage informieren';
+$labels['onlyworkinghours'] = 'Verfügbarkeit innerhalb meiner Arbeitszeiten suchen';
+$labels['reqallattendees'] = 'Erforderliche/alle Teilnehmer';
+$labels['prevslot'] = 'Vorheriger Vorschlag';
+$labels['nextslot'] = 'Nächster Vorschlag';
+$labels['suggestedslot'] = 'Empfohlener Slot';
+$labels['noslotfound'] = 'Es konnten keine freien Zeiten gefunden werden';
+$labels['invitationsubject'] = 'Sie wurden zu "$title" eingeladen';
+$labels['invitationmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit allen Details des Termins. Diese können Sie in Ihre Kalenderanwendung importieren.";
+$labels['invitationattendlinks'] = "Falls Ihr E-Mail-Programm keine iTip-Anfragen unterstützt, können Sie den folgenden Link verwenden, um den Termin zu bestätigen oder abzulehnen:\n\$url";
+$labels['eventupdatesubject'] = '"$title" wurde aktualisiert';
+$labels['eventupdatesubjectempty'] = 'Termin wurde aktualisiert';
+$labels['eventupdatemailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit den aktualisiereten Termindaten. Diese können Sie in Ihre Kalenderanwendung importieren.";
+$labels['eventcancelsubject'] = '"$title" wurde abgesagt';
+$labels['eventcancelmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nDer Termin wurde von \$organizer abgesagt.\n\nIm Anhang finden Sie eine iCalendar-Datei mit den Termindaten.";
+$labels['itipobjectnotfound'] = 'Der Termin auf den sich diese Nachricht bezieht, wurde in Deinem Kalnder nicht gefunden.';
+$labels['itipmailbodyaccepted'] = "\$sender hat die Einladung zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender hat die Einladung mit Vorbehalt zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender hat die Einladung zum folgenden Termin abgelehnt:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender hat Deine Teilnahme bei der folgenden Veranstaltung zurückgewiesen:\n\n*\$title*\n\nam: \$date";
+$labels['itipmailbodydelegated'] = "\$sender hat die Teilnahme an folgendem Event delegiert:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender hat die Teilnahme an folgendem Event an Sie delegiert:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipdeclineevent'] = 'Möchten Sie die Einladung zu diesem Termin ablehnen?';
+$labels['declinedeleteconfirm'] = 'Soll der abgelehnte Termin zusätzlich aus dem Kalender gelöscht werden?';
+$labels['itipcomment'] = 'Kommentar zur Einladungs-/Benachrichtigungsnachricht';
+$labels['itipcommenttitle'] = 'Dieser Kommentar wird an die Einladungs-/Benachrichtigungsnachricht angehängt, die an die Teilnehmer verschickt wird';
+$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgeführt';
+$labels['eventcancelled'] = 'Der Termin wurde vom Organisator abgesagt';
+$labels['saveincalendar'] = 'speichern in';
+$labels['updatemycopy'] = 'In meinen Kalender updaten';
+$labels['savetocalendar'] = 'In Kalender übernehmen';
+$labels['openpreview'] = 'Kalender überprüfen';
+$labels['noearlierevents'] = 'Keine früheren Ereignisse';
+$labels['nolaterevents'] = 'Keine späteren Ereignisse';
+$labels['resource'] = 'Ressource';
+$labels['addresource'] = 'Ressource buchen';
+$labels['findresources'] = 'Ressourcen finden';
+$labels['resourcedetails'] = 'Details';
+$labels['resourceavailability'] = 'Verfügbarkeit';
+$labels['resourceowner'] = 'Eigentümer';
+$labels['resourceadded'] = 'Diese Ressource wurde Deinem Ereignis hinzugefügt';
+$labels['tabsummary'] = 'Übersicht';
+$labels['tabrecurrence'] = 'Wiederholung';
+$labels['tabattendees'] = 'Teilnehmer';
+$labels['tabresources'] = 'Ressourcen';
+$labels['tabattachments'] = 'Anhänge';
+$labels['tabsharing'] = 'Freigabe';
+$labels['deleteobjectconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
+$labels['deleteventconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
+$labels['deletecalendarconfirm'] = 'Möchten Sie diesen Kalender mit allen Terminen wirklich löschen?';
+$labels['deletecalendarconfirmrecursive'] = 'Soll dieser Kalender wirklich mit allen Terminen und Unterkalendern gelöscht werden?';
+$labels['savingdata'] = 'Speichere Daten...';
+$labels['errorsaving'] = 'Fehler beim Speichern.';
+$labels['operationfailed'] = 'Die Aktion ist fehlgeschlagen.';
+$labels['invalideventdates'] = 'Ungültige Daten eingegeben! Bitte überprüfen Sie die Eingaben.';
+$labels['invalidcalendarproperties'] = 'Ungültige Kalenderinformationen! Bitte geben Sie einen Namen ein.';
+$labels['searchnoresults'] = 'Keine Termine in den gewählten Kalendern gefunden.';
+$labels['successremoval'] = 'Der Termin wurde erfolgreich gelöscht.';
+$labels['successrestore'] = 'Der Termin wurde erfolgreich wieder hergestellt.';
+$labels['errornotifying'] = 'Benachrichtigung an die Teilnehmer konnten nicht gesendet werden';
+$labels['errorimportingevent'] = 'Fehler beim Importieren';
+$labels['importwarningexists'] = 'Eine Kopie dieses Termins exisitert bereits in Deinem Kalender.';
+$labels['newerversionexists'] = 'Eine neuere Version dieses Termins exisitert bereits! Import abgebrochen.';
+$labels['nowritecalendarfound'] = 'Kein Kalender zum Speichern gefunden';
+$labels['importedsuccessfully'] = 'Der Termin wurde erfolgreich in \'$calendar\' gespeichert';
+$labels['updatedsuccessfully'] = 'Der Termin wurde erfolgreich in \'$calendar\' geändert';
+$labels['attendeupdateesuccess'] = 'Teilnehmerstatus erfolgreich aktualisiert';
+$labels['itipsendsuccess'] = 'Einladung an Teilnehmer versendet.';
+$labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden';
+$labels['itipinvalidrequest'] = 'Diese Einladung ist nicht mehr gültig.';
+$labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet';
+$labels['localchangeswarning'] = 'Die auszuführenden Änderungen werden sich nur auf den persönlichen Kalender auswirken und nicht an den Organisator des Termins weitergeleitet.';
+$labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert';
+$labels['importnone'] = 'Keine Termine zum Importieren gefunden';
+$labels['importerror'] = 'Fehler beim Importieren';
+$labels['aclnorights'] = 'Der Zugriff auf diesen Kalender erfordert Administrator-Rechte.';
+$labels['changeeventconfirm'] = 'Termin ändern';
+$labels['removeeventconfirm'] = 'Ereignis löschen';
+$labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';
+$labels['removerecurringeventwarning'] = 'Dies ist ein wiederkehrender Termin. Wollen Sie nur diesen Termin bearbeiten oder alle zukünftigen Vorkommen? Alternativ können auch alle Vorkommen bearbeitet werden.';
+$labels['removerecurringallonly'] = 'Dieses ist ein wiederkehrender Termin. Als ein Teilnehmer können Sie nur den gesamten Termin inklusive aller Wiederholungen löschen.';
+$labels['currentevent'] = 'Aktuellen';
+$labels['futurevents'] = 'Zukünftige';
+$labels['allevents'] = 'Alle';
+$labels['saveasnew'] = 'Als neu speichern';
+$labels['birthdays'] = 'Geburtstage';
+$labels['birthdayscalendar'] = 'Geburtstags-Kalender';
+$labels['displaybirthdayscalendar'] = 'Geburtstags-Kalender anzeigen';
+$labels['birthdayscalendarsources'] = 'Für diese Adressbücher';
+$labels['birthdayeventtitle'] = '$names Geburtstag';
+$labels['birthdayage'] = 'Alter $age';
+$labels['objectchangelog'] = 'Änderungshistorie';
+$labels['objectdiff'] = 'Änderungen aus $rev1 nach $rev2';
+$labels['revision'] = 'Version';
+$labels['user'] = 'Benutzer';
+$labels['operation'] = 'Aktion';
+$labels['actionappend'] = 'Gespeichert';
+$labels['actionmove'] = 'Verschoben';
+$labels['actiondelete'] = 'Gelöscht';
+$labels['compare'] = 'Vergleichen';
+$labels['showrevision'] = 'Diese Version anzeigen';
+$labels['restore'] = 'Diese Version wiederherstellen';
+$labels['objectnotfound'] = 'Termindaten sind leider nicht vergübar';
+$labels['objectchangelognotavailable'] = 'Änderungshistorie ist nicht verfügbar für diesen Termin';
+$labels['objectdiffnotavailable'] = 'Vergleich für die gewählten Versionen nicht möglich';
+$labels['revisionrestoreconfirm'] = 'Wollen Sie wirklich die Version $rev dieses Termins wiederherstellen? Diese Aktion wird die aktuelle Kopie mit der älteren Version ersetzen.';
+$labels['objectrestoresuccess'] = 'Revision $rev erfolgreich wiederhergestellt';
+$labels['objectrestoreerror'] = 'Fehler beim Wiederherstellen der alten Revision';
+$labels['arialabelminical'] = 'Kalender Datumswahl';
+$labels['arialabelcalendarview'] = 'Kalender Ansicht';
+$labels['arialabelsearchform'] = 'Suchformular für Termine';
+$labels['arialabelquicksearchbox'] = 'Sucheingabe für Termine';
+$labels['arialabelcalsearchform'] = 'Suchformular für Kalender';
+$labels['calendaractions'] = 'Kalenderaktionen';
+$labels['arialabeleventattendees'] = 'Teilehmerliste';
+$labels['arialabeleventresources'] = 'Liste der Terminressourcen';
+$labels['arialabelresourcesearchform'] = 'Suchformular für Ressourcen';
+$labels['arialabelresourceselection'] = 'Verfügbare Ressourcen';
+
+$labels['calendar_database'] = 'Datenbank Kalender';
+$labels['calendar_kolab'] = 'Kolab Kalender';
+$labels['calendar_caldav'] = 'CalDAV Kalender';
+$labels['calendar_ical'] = 'iCAL Kalender';
+$labels['caldavurl'] = "CalDAV URL";
+$labels['icalurl'] = "iCal URL";
+?>
diff --git a/calendar/localization/en_US.inc b/calendar/localization/en_US.inc
new file mode 100644
index 0000000..e2bada3
--- /dev/null
+++ b/calendar/localization/en_US.inc
@@ -0,0 +1,315 @@
+<?php
+
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+
+$labels = array();
+
+// preferences
+$labels['default_view'] = 'Default view';
+$labels['time_format'] = 'Time format';
+$labels['timeslots'] = 'Time slots per hour';
+$labels['first_day'] = 'First weekday';
+$labels['first_hour'] = 'First hour to show';
+$labels['workinghours'] = 'Working hours';
+$labels['add_category'] = 'Add category';
+$labels['remove_category'] = 'Remove category';
+$labels['defaultcalendar'] = 'Create new events in';
+$labels['eventcoloring'] = 'Event coloring';
+$labels['coloringmode0'] = 'According to calendar';
+$labels['coloringmode1'] = 'According to category';
+$labels['coloringmode2'] = 'Calendar for outline, category for content';
+$labels['coloringmode3'] = 'Category for outline, calendar for content';
+$labels['afternothing'] = 'Do nothing';
+$labels['aftertrash'] = 'Move to Trash';
+$labels['afterdelete'] = 'Delete the message';
+$labels['afterflagdeleted'] = 'Flag as deleted';
+$labels['aftermoveto'] = 'Move to...';
+$labels['itipoptions'] = 'Event Invitations';
+$labels['afteraction'] = 'After an invitation or update message is processed';
+
+// calendar
+$labels['calendar'] = 'Calendar';
+$labels['calendars'] = 'Calendars';
+$labels['category'] = 'Category';
+$labels['categories'] = 'Categories';
+$labels['createcalendar'] = 'Create new calendar';
+$labels['editcalendar'] = 'Edit calendar properties';
+$labels['name'] = 'Name';
+$labels['color'] = 'Color';
+$labels['day'] = 'Day';
+$labels['week'] = 'Week';
+$labels['month'] = 'Month';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'New';
+$labels['new_event'] = 'New event';
+$labels['edit_event'] = 'Edit event';
+$labels['edit'] = 'Edit';
+$labels['save'] = 'Save';
+$labels['removelist'] = 'Remove from list';
+$labels['cancel'] = 'Cancel';
+$labels['select'] = 'Select';
+$labels['print'] = 'Print';
+$labels['printtitle'] = 'Print calendars';
+$labels['title'] = 'Summary';
+$labels['description'] = 'Description';
+$labels['all-day'] = 'all-day';
+$labels['export'] = 'Export';
+$labels['exporttitle'] = 'Export to iCalendar';
+$labels['exportrange'] = 'Events from';
+$labels['exportattachments'] = 'With attachments';
+$labels['customdate'] = 'Custom date';
+$labels['location'] = 'Location';
+$labels['url'] = 'URL';
+$labels['date'] = 'Date';
+$labels['start'] = 'Start';
+$labels['starttime'] = 'Start time';
+$labels['end'] = 'End';
+$labels['endtime'] = 'End time';
+$labels['repeat'] = 'Repeat';
+$labels['selectdate'] = 'Choose date';
+$labels['freebusy'] = 'Show me as';
+$labels['free'] = 'Free';
+$labels['busy'] = 'Busy';
+$labels['outofoffice'] = 'Out of Office';
+$labels['tentative'] = 'Tentative';
+$labels['mystatus'] = 'My status';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Confirmed';
+$labels['status-cancelled'] = 'Cancelled';
+$labels['priority'] = 'Priority';
+$labels['sensitivity'] = 'Privacy';
+$labels['public'] = 'public';
+$labels['private'] = 'private';
+$labels['confidential'] = 'confidential';
+$labels['links'] = 'Reference';
+$labels['alarms'] = 'Reminder';
+$labels['comment'] = 'Comment';
+$labels['created'] = 'Created';
+$labels['changed'] = 'Last Modified';
+$labels['unknown'] = 'Unknown';
+$labels['eventoptions'] = 'Options';
+$labels['generated'] = 'generated at';
+$labels['eventhistory'] = 'History';
+$labels['removelink'] = 'Remove email reference';
+$labels['printdescriptions'] = 'Print descriptions';
+$labels['parentcalendar'] = 'Insert inside';
+$labels['searchearlierdates'] = '« Search for earlier events';
+$labels['searchlaterdates'] = 'Search for later events »';
+$labels['andnmore'] = '$nr more...';
+$labels['togglerole'] = 'Click to toggle role';
+$labels['createfrommail'] = 'Save as event';
+$labels['importevents'] = 'Import events';
+$labels['importrange'] = 'Events from';
+$labels['onemonthback'] = '1 month back';
+$labels['nmonthsback'] = '$nr months back';
+$labels['showurl'] = 'Show calendar URL';
+$labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
+$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
+$labels['findcalendars'] = 'Find calendars...';
+$labels['searchterms'] = 'Search terms';
+$labels['calsearchresults'] = 'Available Calendars';
+$labels['calendarsubscribe'] = 'List permanently';
+$labels['nocalendarsfound'] = 'No calendars found';
+$labels['nrcalendarsfound'] = '$nr calendars found';
+$labels['quickview'] = 'View only this calendar';
+$labels['invitationspending'] = 'Pending invitations';
+$labels['invitationsdeclined'] = 'Declined invitations';
+$labels['changepartstat'] = 'Change participant status';
+$labels['rsvpcomment'] = 'Invitation text';
+
+// agenda view
+$labels['listrange'] = 'Range to display:';
+$labels['listsections'] = 'Divide into:';
+$labels['smartsections'] = 'Smart sections';
+$labels['until'] = 'until';
+$labels['today'] = 'Today';
+$labels['tomorrow'] = 'Tomorrow';
+$labels['thisweek'] = 'This week';
+$labels['nextweek'] = 'Next week';
+$labels['prevweek'] = 'Previous week';
+$labels['thismonth'] = 'This month';
+$labels['nextmonth'] = 'Next month';
+$labels['weekofyear'] = 'Week';
+$labels['pastevents'] = 'Past';
+$labels['futureevents'] = 'Future';
+
+// alarm/reminder settings
+$labels['showalarms'] = 'Show reminders';
+$labels['defaultalarmtype'] = 'Default reminder setting';
+$labels['defaultalarmoffset'] = 'Default reminder time';
+
+// attendees
+$labels['attendee'] = 'Participant';
+$labels['role'] = 'Role';
+$labels['availability'] = 'Avail.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Add participant';
+$labels['roleorganizer'] = 'Organizer';
+$labels['rolerequired'] = 'Required';
+$labels['roleoptional'] = 'Optional';
+$labels['rolechair'] = 'Chair';
+$labels['rolenonparticipant'] = 'Absent';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Group';
+$labels['cutyperesource'] = 'Resource';
+$labels['cutyperoom'] = 'Room';
+$labels['availfree'] = 'Free';
+$labels['availbusy'] = 'Busy';
+$labels['availunknown'] = 'Unknown';
+$labels['availtentative'] = 'Tentative';
+$labels['availoutofoffice'] = 'Out of Office';
+$labels['delegatedto'] = 'Delegated to: ';
+$labels['delegatedfrom'] = 'Delegated from: ';
+$labels['scheduletime'] = 'Find availability';
+$labels['sendinvitations'] = 'Send invitations';
+$labels['sendnotifications'] = 'Notify participants about modifications';
+$labels['sendcancellation'] = 'Notify participants about event cancellation';
+$labels['onlyworkinghours'] = 'Find availability within my working hours';
+$labels['reqallattendees'] = 'Required/all participants';
+$labels['prevslot'] = 'Previous Slot';
+$labels['nextslot'] = 'Next Slot';
+$labels['suggestedslot'] = 'Suggested Slot';
+$labels['noslotfound'] = 'Unable to find a free time slot';
+$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application.";
+$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url";
+$labels['eventupdatesubject'] = '"$title" has been updated';
+$labels['eventupdatesubjectempty'] = 'An event that concerns you has been updated';
+$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated event details which you can import to your calendar application.";
+$labels['eventcancelsubject'] = '"$title" has been canceled';
+$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe event has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated event details.";
+
+// invitation handling (overrides labels from libcalendaring)
+$labels['itipobjectnotfound'] = 'The event referred by this message was not found in your calendar.';
+
+$labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender has declined the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegated'] = "\$sender has delegated the participation in the following event:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender has delegated the participation in the following event to you:\n\n*\$title*\n\nWhen: \$date";
+
+$labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
+$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
+
+$labels['notanattendee'] = 'You\'re not listed as an attendee of this event';
+$labels['eventcancelled'] = 'The event has been cancelled';
+$labels['saveincalendar'] = 'save in';
+$labels['updatemycopy'] = 'Update in my calendar';
+$labels['savetocalendar'] = 'Save to calendar';
+$labels['openpreview'] = 'Check Calendar';
+$labels['noearlierevents'] = 'No earlier events';
+$labels['nolaterevents'] = 'No later events';
+
+// resources
+$labels['resource'] = 'Resource';
+$labels['addresource'] = 'Book resource';
+$labels['findresources'] = 'Find resources';
+$labels['resourcedetails'] = 'Details';
+$labels['resourceavailability'] = 'Availability';
+$labels['resourceowner'] = 'Owner';
+$labels['resourceadded'] = 'The resource was added to your event';
+
+// event dialog tabs
+$labels['tabsummary'] = 'Summary';
+$labels['tabrecurrence'] = 'Recurrence';
+$labels['tabattendees'] = 'Participants';
+$labels['tabresources'] = 'Resources';
+$labels['tabattachments'] = 'Attachments';
+$labels['tabsharing'] = 'Sharing';
+
+// messages
+$labels['deleteobjectconfirm'] = 'Do you really want to delete this event?';
+$labels['deleteventconfirm'] = 'Do you really want to delete this event?';
+$labels['deletecalendarconfirm'] = 'Do you really want to delete this calendar with all its events?';
+$labels['deletecalendarconfirmrecursive'] = 'Do you really want to delete this calendar with all its events and sub-calendars?';
+$labels['savingdata'] = 'Saving data...';
+$labels['errorsaving'] = 'Failed to save changes.';
+$labels['operationfailed'] = 'The requested operation failed.';
+$labels['invalideventdates'] = 'Invalid dates entered! Please check your input.';
+$labels['invalidcalendarproperties'] = 'Invalid calendar properties! Please set a valid name.';
+$labels['searchnoresults'] = 'No events found in the selected calendars.';
+$labels['successremoval'] = 'The event has been deleted successfully.';
+$labels['successrestore'] = 'The event has been restored successfully.';
+$labels['errornotifying'] = 'Failed to send notifications to event participants';
+$labels['errorimportingevent'] = 'Failed to import the event';
+$labels['importwarningexists'] = 'A copy of this event already exists in your calendar.';
+$labels['newerversionexists'] = 'A newer version of this event already exists! Aborted.';
+$labels['nowritecalendarfound'] = 'No calendar found to save the event';
+$labels['importedsuccessfully'] = 'The event was successfully added to \'$calendar\'';
+$labels['updatedsuccessfully'] = 'The event was successfully updated in \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
+$labels['itipsendsuccess'] = 'Invitation sent to participants.';
+$labels['itipresponseerror'] = 'Failed to send the response to this event invitation';
+$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
+$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
+$labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your calendar and not be sent to the organizer of the event.';
+$labels['importsuccess'] = 'Successfully imported $nr events';
+$labels['importnone'] = 'No events found to be imported';
+$labels['importerror'] = 'An error occured while importing';
+$labels['aclnorights'] = 'You do not have administrator rights on this calendar.';
+
+$labels['changeeventconfirm'] = 'Change event';
+$labels['removeeventconfirm'] = 'Delete event';
+$labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?';
+$labels['removerecurringeventwarning'] = 'This is a recurring event. Would you like to delete the current event only, this and all future occurences or all occurences of this event?';
+$labels['removerecurringallonly'] = 'This is a recurring event. As a participant, you can only delete the entire event with all occurences.';
+$labels['currentevent'] = 'Current';
+$labels['futurevents'] = 'Future';
+$labels['allevents'] = 'All';
+$labels['saveasnew'] = 'Save as new';
+
+// birthdays calendar
+$labels['birthdays'] = 'Birthdays';
+$labels['birthdayscalendar'] = 'Birthdays Calendar';
+$labels['displaybirthdayscalendar'] = 'Display birthdays calendar';
+$labels['birthdayscalendarsources'] = 'From these address books';
+$labels['birthdayeventtitle'] = '$name\'s Birthday';
+$labels['birthdayage'] = 'Age $age';
+
+// history dialog
+$labels['objectchangelog'] = 'Change History';
+$labels['objectdiff'] = 'Changes from $rev1 to $rev2';
+$labels['revision'] = 'Revision';
+$labels['user'] = 'User';
+$labels['operation'] = 'Action';
+$labels['actionappend'] = 'Saved';
+$labels['actionmove'] = 'Moved';
+$labels['actiondelete'] = 'Deleted';
+$labels['compare'] = 'Compare';
+$labels['showrevision'] = 'Show this version';
+$labels['restore'] = 'Restore this version';
+$labels['objectnotfound'] = 'Failed to load event data';
+$labels['objectchangelognotavailable'] = 'Change history is not available for this event';
+$labels['objectdiffnotavailable'] = 'No comparison possible for the selected revisions';
+$labels['revisionrestoreconfirm'] = 'Do you really want to restore revision $rev of this event? This will replace the current event with the old version.';
+$labels['objectrestoresuccess'] = 'Revision $rev successfully restored';
+$labels['objectrestoreerror'] = 'Failed to restore the old revision';
+
+
+// (hidden) titles and labels for accessibility annotations
+$labels['arialabelminical'] = 'Calendar date selection';
+$labels['arialabelcalendarview'] = 'Calendar view';
+$labels['arialabelsearchform'] = 'Event search form';
+$labels['arialabelquicksearchbox'] = 'Event search input';
+$labels['arialabelcalsearchform'] = 'Calendars search form';
+$labels['calendaractions'] = 'Calendar actions';
+$labels['arialabeleventattendees'] = 'Event participants list';
+$labels['arialabeleventresources'] = 'Event resources list';
+$labels['arialabelresourcesearchform'] = 'Resources search form';
+$labels['arialabelresourceselection'] = 'Available resources';
+
+$labels['calendar_kolab'] = 'Kolab Calender';
+$labels['calendar_caldav'] = 'CalDAV Calender';
+$labels['calendar_ical'] = 'iCAL Calender';
+$labels['caldavurl'] = "CalDAV URL";
+$labels['icalurl'] = "iCal URL";
+?>
diff --git a/calendar/localization/es_AR.inc b/calendar/localization/es_AR.inc
new file mode 100644
index 0000000..2a66a0f
--- /dev/null
+++ b/calendar/localization/es_AR.inc
@@ -0,0 +1,269 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Vista predeterminada';
+$labels['time_format'] = 'Formato de hora';
+$labels['timeslots'] = 'Espacios por hora';
+$labels['first_day'] = 'Primer día de semana';
+$labels['first_hour'] = 'Primer hora a mostrar';
+$labels['workinghours'] = 'Horario laboral';
+$labels['add_category'] = 'Agregar categoría';
+$labels['remove_category'] = 'Eliminar categoría';
+$labels['defaultcalendar'] = 'Crear nuevos eventos en';
+$labels['eventcoloring'] = 'Colores de eventos';
+$labels['coloringmode0'] = 'De acuerdo al calendario';
+$labels['coloringmode1'] = 'De acuerdo a la categoría';
+$labels['coloringmode2'] = 'Calendario para borde, categoría para contenido';
+$labels['coloringmode3'] = 'Categoría para borde, calendario para contenido';
+$labels['afternothing'] = 'Hacer nada';
+$labels['aftertrash'] = 'Mover a la papelera';
+$labels['afterdelete'] = 'Eliminar el mensaje';
+$labels['afterflagdeleted'] = 'Marcar como eliminado';
+$labels['aftermoveto'] = 'Mover a...';
+$labels['itipoptions'] = 'Invitaciones del evento';
+$labels['afteraction'] = 'Luego que una invitación o actualización de mensaje es procesado';
+$labels['calendar'] = 'Calendario';
+$labels['calendars'] = 'Calendarios';
+$labels['category'] = 'Categoría';
+$labels['categories'] = 'Categorías';
+$labels['createcalendar'] = 'Crear nuevo calendario';
+$labels['editcalendar'] = 'Editar propiedades del calendario';
+$labels['name'] = 'Nombre';
+$labels['color'] = 'Color';
+$labels['day'] = 'Día';
+$labels['week'] = 'Semana';
+$labels['month'] = 'Mes';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nuevo';
+$labels['new_event'] = 'Nuevo evento';
+$labels['edit_event'] = 'Editar evento';
+$labels['edit'] = 'Editar';
+$labels['save'] = 'Guardar';
+$labels['removelist'] = 'Eliminar de la lista';
+$labels['cancel'] = 'Cancelar';
+$labels['select'] = 'Seleccionar';
+$labels['print'] = 'Imprimir';
+$labels['printtitle'] = 'Imprimir calendarios';
+$labels['title'] = 'Sumario';
+$labels['description'] = 'Descripción';
+$labels['all-day'] = 'Todo el día';
+$labels['export'] = 'Exportar';
+$labels['exporttitle'] = 'Exportar a iCalendar';
+$labels['exportrange'] = 'Eventos de';
+$labels['exportattachments'] = 'Con adjunto';
+$labels['customdate'] = 'Fecha personalizada';
+$labels['location'] = 'Localización';
+$labels['url'] = 'URL';
+$labels['date'] = 'Fecha';
+$labels['start'] = 'Inicio';
+$labels['starttime'] = 'Hora de inicio';
+$labels['end'] = 'Fin';
+$labels['endtime'] = 'Hora de finalización';
+$labels['repeat'] = 'Repetir';
+$labels['selectdate'] = 'Elegir fecha';
+$labels['freebusy'] = 'Mostrarme como';
+$labels['free'] = 'Libre';
+$labels['busy'] = 'Ocupado';
+$labels['outofoffice'] = 'Fuera de la oficina';
+$labels['tentative'] = 'Tentativo';
+$labels['mystatus'] = 'Mi estado';
+$labels['status'] = 'Estado';
+$labels['status-confirmed'] = 'Confirmado';
+$labels['status-cancelled'] = 'Cancelado';
+$labels['priority'] = 'Prioridad';
+$labels['sensitivity'] = 'Privacidad';
+$labels['public'] = 'público';
+$labels['private'] = 'privado';
+$labels['confidential'] = 'confidencial';
+$labels['alarms'] = 'Recordatorio';
+$labels['comment'] = 'Comentario';
+$labels['created'] = 'Creado';
+$labels['changed'] = 'Última modificación';
+$labels['unknown'] = 'Desconocido';
+$labels['eventoptions'] = 'Opciones';
+$labels['generated'] = 'generado en';
+$labels['eventhistory'] = 'Historial';
+$labels['printdescriptions'] = 'Imprimir descripciones';
+$labels['parentcalendar'] = 'Insertar dentro';
+$labels['searchearlierdates'] = '« Buscar eventos anteriores';
+$labels['searchlaterdates'] = 'Buscar eventos posteriores »';
+$labels['andnmore'] = '$nr más...';
+$labels['togglerole'] = 'Click para cambiar rol';
+$labels['createfrommail'] = 'Guardar como evento';
+$labels['importevents'] = 'Importar eventos';
+$labels['importrange'] = 'Eventos de';
+$labels['onemonthback'] = '1 mes atrás';
+$labels['nmonthsback'] = '$nr meses atrás';
+$labels['showurl'] = 'Mostrar URL del calendario';
+$labels['showurldescription'] = 'Use la siguiente direccion para acceder (sólo lectura) a su calendario desde otras aplicaciones. Puede copiar y pegar esto dentro de cualquier software de calendario que soporte el formato iCal.';
+$labels['caldavurldescription'] = 'Copie esta direccion a su aplicación cliente <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> (por ejemplo, Evolution o Mozilla Thunderbird) para sincronizar completamente este calendario específico con su ordenador o dispositivo móvil.';
+$labels['findcalendars'] = 'Encontrar calendarios...';
+$labels['searchterms'] = 'Buscar términos';
+$labels['calsearchresults'] = 'Calendarios disponibles';
+$labels['calendarsubscribe'] = 'Listar permanentemente';
+$labels['nocalendarsfound'] = 'No se encontraron calendarios';
+$labels['nrcalendarsfound'] = '$nr calendarios encontrados';
+$labels['quickview'] = 'Ver sólo este calendario';
+$labels['invitationspending'] = 'Invitaciones pendientes';
+$labels['invitationsdeclined'] = 'Invitaciones rechazadas';
+$labels['changepartstat'] = 'Cambiar el estado del participante';
+$labels['rsvpcomment'] = 'Texto de invitación';
+$labels['listrange'] = 'Rango a mostrar:';
+$labels['listsections'] = 'Dividir en:';
+$labels['smartsections'] = 'Secciones inteligentes';
+$labels['until'] = 'hasta';
+$labels['today'] = 'Hoy';
+$labels['tomorrow'] = 'Mañana';
+$labels['thisweek'] = 'Esta semana';
+$labels['nextweek'] = 'Próxima semana';
+$labels['prevweek'] = 'Semana anterior';
+$labels['thismonth'] = 'Este mes';
+$labels['nextmonth'] = 'Próximo mes';
+$labels['weekofyear'] = 'Semana';
+$labels['pastevents'] = 'Pasado';
+$labels['futureevents'] = 'Futuro';
+$labels['showalarms'] = 'Mostrar alarmas';
+$labels['defaultalarmtype'] = 'Configuración predeterminada de recordatorio';
+$labels['defaultalarmoffset'] = 'Tiempo predeterminado de recordatorio';
+$labels['attendee'] = 'Participante';
+$labels['role'] = 'Rol';
+$labels['availability'] = 'Disp.';
+$labels['confirmstate'] = 'Estado';
+$labels['addattendee'] = 'Agregar participante';
+$labels['roleorganizer'] = 'Organizador';
+$labels['rolerequired'] = 'Requerido';
+$labels['roleoptional'] = 'Opcional';
+$labels['rolechair'] = 'Jefe';
+$labels['rolenonparticipant'] = 'Ausente';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Grupo';
+$labels['cutyperesource'] = 'Recurso';
+$labels['cutyperoom'] = 'Habitación';
+$labels['availfree'] = 'Libre';
+$labels['availbusy'] = 'Ocupado';
+$labels['availunknown'] = 'Desconocido';
+$labels['availtentative'] = 'Tentativo';
+$labels['availoutofoffice'] = 'Fuera de la oficina';
+$labels['delegatedto'] = 'Delegado a:';
+$labels['delegatedfrom'] = 'Delegado de:';
+$labels['scheduletime'] = 'Buscar disponibilidad';
+$labels['sendinvitations'] = 'Enviar invitaciones';
+$labels['sendnotifications'] = 'Notificar a los participantes sobre las modificaciones';
+$labels['sendcancellation'] = 'Notificar a los participantes sobre la cancelación del evento';
+$labels['onlyworkinghours'] = 'Buscar disponibilidad dentro de mi horario laboral';
+$labels['reqallattendees'] = 'Requerido/todos los participantes';
+$labels['prevslot'] = 'Espacio anterior';
+$labels['nextslot'] = 'Espacio siguiente';
+$labels['suggestedslot'] = 'Espacio sugerido';
+$labels['noslotfound'] = 'Imposible encontrar un espacio libre';
+$labels['invitationsubject'] = 'Ha sido invitado a "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nCuándo: \$date\n\nInvitados: \$attendees\n\nEncontrará adjunto un archivo iCalendar con todos los detalles del evento, el cual puede importar a su aplicación de calendario.";
+$labels['invitationattendlinks'] = "En caso que su cliente de correo electrónico no soporte peticiones iTip puede usar el siguiente link para aceptar o declinar esta invitación:\n\$url";
+$labels['eventupdatesubject'] = '"$title" ha sido actualizado';
+$labels['eventupdatesubjectempty'] = 'Un evento que le interesa ha sido actualizado';
+$labels['eventupdatemailbody'] = "*\$title*\n\nCuándo: \$date\n\nInvitados: \$attendees\n\nEncontrará adjunto un archivo iCalendar con todos los detalles del evento, el cual puede importar a su aplicación de calendario.";
+$labels['eventcancelsubject'] = '"$title" has been canceled';
+$labels['eventcancelmailbody'] = "*\$title*\n\nCuándo: \$date\n\nInvitados: \$attendees\n\nEl evento ha sido cancelado por \$organizer.\n\nEncontrará adjunto un archivo iCalendar con todos los detalles actualizados del evento.";
+$labels['itipobjectnotfound'] = 'El evento referido por este mensaje no fue encontrado en su calendario.';
+$labels['itipmailbodyaccepted'] = "\$sender ha aceptado la invitación al siguiente evento:\n\n*\$title*\n\nCuándo: \$date\n\nInvitados: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender ha tentativamente aceptado la invitación al siguiente evento:\n\n*\$title*\n\nCuándo: \$date\n\nInvitados: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender ha rechazado la invitación al siguiente evento:\n\n*\$title*\n\nCuándo: \$date\n\nInvitados: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender ha rechazado tu participación en el siguiente evento:\n\n*\$title*\n\nCuándo:\$date";
+$labels['itipdeclineevent'] = '¿Quiere rechazar la invitación a este evento?';
+$labels['declinedeleteconfirm'] = '¿Quiere también eliminar este evento rechazado de su calendario?';
+$labels['itipcomment'] = 'Comentario de la invitación/notificación';
+$labels['itipcommenttitle'] = 'Este comentario será adjuntado al mensaje de invitación/notificación enviado a los participantes';
+$labels['notanattendee'] = 'No esta incluído en la lista de invitados a este evento';
+$labels['eventcancelled'] = 'El evento ha sido cancelado';
+$labels['saveincalendar'] = 'guardar en';
+$labels['updatemycopy'] = 'Actualizar mi calendario';
+$labels['savetocalendar'] = 'Guardar en el calendario';
+$labels['openpreview'] = 'Comprobar Calendario';
+$labels['noearlierevents'] = 'No hay eventos anteriores';
+$labels['nolaterevents'] = 'No hay eventos posteriores';
+$labels['resource'] = 'Recurso';
+$labels['addresource'] = 'Agendar recurso';
+$labels['findresources'] = 'Encontrar recursos';
+$labels['resourcedetails'] = 'Detalles';
+$labels['resourceavailability'] = 'Disponibilidad';
+$labels['resourceowner'] = 'Propietario';
+$labels['resourceadded'] = 'El recurso fue agregado a su evento';
+$labels['tabsummary'] = 'Sumario';
+$labels['tabrecurrence'] = 'Recurrencia';
+$labels['tabattendees'] = 'Participantes';
+$labels['tabresources'] = 'Recursos';
+$labels['tabattachments'] = 'Adjuntos';
+$labels['tabsharing'] = 'Compartir';
+$labels['deleteobjectconfirm'] = 'Confirme que desea eliminar este evento';
+$labels['deleteventconfirm'] = 'Confirme que desea eliminar este evento';
+$labels['deletecalendarconfirm'] = 'Confirme que desea eliminar este calendario con todos sus eventos';
+$labels['deletecalendarconfirmrecursive'] = 'Confirme que desea eliminar este calendario con todos sus eventos y sub-calendarios';
+$labels['savingdata'] = 'Guardando...';
+$labels['errorsaving'] = 'Falló guardando los cambios.';
+$labels['operationfailed'] = 'La operación falló.';
+$labels['invalideventdates'] = 'Ingresó fechas erroneas. Por favor compruebe los datos.';
+$labels['invalidcalendarproperties'] = 'Propiedades del calendario erroneas. Por favor ingrese un nombre válido.';
+$labels['searchnoresults'] = 'No hay eventos en los calendarios seleccionados.';
+$labels['successremoval'] = 'El evento ha sido eliminado exitosamente.';
+$labels['successrestore'] = 'El evento ha sido recuperado exitosamente.';
+$labels['errornotifying'] = 'Fallo al enviar las notificaciones del evento a los participantes';
+$labels['errorimportingevent'] = 'Fallo al importar el evento';
+$labels['importwarningexists'] = 'Una copia de este evento ya existe en su calendario.';
+$labels['newerversionexists'] = 'Ya existe una versión actualizada de este evento. Cancelado.';
+$labels['nowritecalendarfound'] = 'No hay calendarios para guardar el evento.';
+$labels['importedsuccessfully'] = 'El evento fue guardado en \'$calendar\' exitosamente';
+$labels['updatedsuccessfully'] = 'El evento fue actualizado exitosamente en \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Se actualizaron los estados de los participantes exitosamente';
+$labels['itipsendsuccess'] = 'Invitaciones enviadas a los participantes.';
+$labels['itipresponseerror'] = 'Fallo enviando la respuesta a la invitación de este evento';
+$labels['itipinvalidrequest'] = 'Esta invitación no es válida';
+$labels['sentresponseto'] = 'Se envió la respuesta a la invitación $mailto exitosamente';
+$labels['localchangeswarning'] = 'Se realizarán cambios que sólo serán reflejadas en su calendario y no serán enviadas al organizador del evento';
+$labels['importsuccess'] = 'Importados $nr eventos exitosamente';
+$labels['importnone'] = 'No se importaron eventos';
+$labels['importerror'] = 'Fallo al importar';
+$labels['aclnorights'] = 'No tiene permisos de administrador en este calendario.';
+$labels['changeeventconfirm'] = 'Cambiar evento';
+$labels['removeeventconfirm'] = 'Eliminar evento';
+$labels['changerecurringeventwarning'] = 'Este es un evento recurrente. ¿Desea editar solo el evento actual, este y las ocurrencias futuras, todas las ocurrencias o guardarlo como un evento nuevo?';
+$labels['removerecurringeventwarning'] = 'Este es un evento recurrente. ¿Desea eliminar solo el evento actual, este y las ocurrencias futuras o todas las ocurrencias del evento?';
+$labels['currentevent'] = 'Actual';
+$labels['futurevents'] = 'Futuro';
+$labels['allevents'] = 'Todos';
+$labels['saveasnew'] = 'Guardar como nuevo';
+$labels['birthdays'] = 'Cumpleaños';
+$labels['birthdayscalendar'] = 'Calendario de cumpleaños';
+$labels['displaybirthdayscalendar'] = 'Mostrar calendario de cumpleaños';
+$labels['birthdayscalendarsources'] = 'De estas libretas de direcciones';
+$labels['birthdayeventtitle'] = 'Cumpleaños de $name';
+$labels['birthdayage'] = 'Edad $age';
+$labels['objectchangelog'] = 'Cambiar Historial';
+$labels['revision'] = 'Revisión';
+$labels['user'] = 'Usuario';
+$labels['operation'] = 'Acción';
+$labels['actionappend'] = 'Guardado';
+$labels['actionmove'] = 'Movido';
+$labels['actiondelete'] = 'Eliminado';
+$labels['compare'] = 'Comparar';
+$labels['showrevision'] = 'Mostrar esta versión';
+$labels['restore'] = 'Recuperar esta versión';
+$labels['objectnotfound'] = 'Fallo al cargar datos del evento';
+$labels['objectchangelognotavailable'] = 'Cambiar historial no esta disponible para este evento';
+$labels['objectdiffnotavailable'] = 'No es posible comparar las revisiones seleccionadas';
+$labels['revisionrestoreconfirm'] = 'Confirme que quiere recuperar la revisión $rev de este evento. Esta acción reemplazará el evento actual con la versión anterior.';
+$labels['arialabelminical'] = 'Selección de fecha del calendario';
+$labels['arialabelcalendarview'] = 'Vista del calendario';
+$labels['arialabelsearchform'] = 'Formulario de búsqueda de evento';
+$labels['arialabelquicksearchbox'] = 'Entrada de búsqueda de evento';
+$labels['arialabelcalsearchform'] = 'Formulario de búsqueda de calendario';
+$labels['calendaractions'] = 'Acciones del calendario';
+$labels['arialabeleventattendees'] = 'Lista de participantes del evento';
+$labels['arialabeleventresources'] = 'Lista de recursos del evento';
+$labels['arialabelresourcesearchform'] = 'Formulario de búsqueda de recursos';
+$labels['arialabelresourceselection'] = 'Recursos disponibles';
+?>
diff --git a/calendar/localization/es_ES.inc b/calendar/localization/es_ES.inc
new file mode 100644
index 0000000..1b025fb
--- /dev/null
+++ b/calendar/localization/es_ES.inc
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['calendars'] = 'Calendarios';
+$labels['name'] = 'Nombre';
+$labels['edit'] = 'Editar';
+$labels['save'] = 'Guardar';
+$labels['cancel'] = 'Cancelar';
+$labels['comment'] = 'Comentario';
+$labels['eventoptions'] = 'Opciones';
+$labels['rolerequired'] = 'Requerido';
+$labels['roleoptional'] = 'Opcional';
+$labels['cutypegroup'] = 'Grupo';
+$labels['cutyperesource'] = 'Recurso';
+$labels['resource'] = 'Recurso';
+$labels['resourcedetails'] = 'Detalles';
+$labels['tabresources'] = 'Recursos';
+$labels['savingdata'] = 'Guardando datos...';
+$labels['user'] = 'Usuario';
+$labels['operation'] = 'Acción';
+?>
diff --git a/calendar/localization/fi_FI.inc b/calendar/localization/fi_FI.inc
new file mode 100644
index 0000000..2d67e4e
--- /dev/null
+++ b/calendar/localization/fi_FI.inc
@@ -0,0 +1,274 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Oletusnäkymä';
+$labels['time_format'] = 'Aikamuoto';
+$labels['timeslots'] = 'Ajankohdat per tunti';
+$labels['first_day'] = 'Viikon ensimmäinen päivä';
+$labels['first_hour'] = 'Ensimmäinen näytettävä tunti';
+$labels['workinghours'] = 'Työaika';
+$labels['add_category'] = 'Lisää luokka';
+$labels['remove_category'] = 'Poista luokka';
+$labels['defaultcalendar'] = 'Luo uudet tapahtumat kohteeseen';
+$labels['eventcoloring'] = 'Tapahtuman väritys';
+$labels['coloringmode0'] = 'Kalenterin mukaan';
+$labels['coloringmode1'] = 'Luokan mukaan';
+$labels['afternothing'] = 'Älä tee mitään';
+$labels['aftertrash'] = 'Siirrä roskakoriin';
+$labels['afterdelete'] = 'Poista viesti';
+$labels['afterflagdeleted'] = 'Merkitse poistettavaksi';
+$labels['aftermoveto'] = 'Siirrä...';
+$labels['itipoptions'] = 'Tapahtuman kutsut';
+$labels['afteraction'] = 'Kun kutsu tai päivitysviesti on käsitelty';
+$labels['calendar'] = 'Kalenteri';
+$labels['calendars'] = 'Kalenterit';
+$labels['category'] = 'Luokka';
+$labels['categories'] = 'Luokat';
+$labels['createcalendar'] = 'Luo uusi kalenteri';
+$labels['editcalendar'] = 'Muokkaa kalenterin ominaisuuksia';
+$labels['name'] = 'Nimi';
+$labels['color'] = 'Väri';
+$labels['day'] = 'Päivä';
+$labels['week'] = 'Viikko';
+$labels['month'] = 'Kuukausi';
+$labels['agenda'] = 'Asialista';
+$labels['new'] = 'Uusi';
+$labels['new_event'] = 'Uusi tapahtuma';
+$labels['edit_event'] = 'Muokkaa tapahtumaa';
+$labels['edit'] = 'Muokkaa';
+$labels['save'] = 'Tallenna';
+$labels['removelist'] = 'Poista listasta';
+$labels['cancel'] = 'Peru';
+$labels['select'] = 'Valitse';
+$labels['print'] = 'Tulosta';
+$labels['printtitle'] = 'Tulosta kalenterit';
+$labels['title'] = 'Yhteenveto';
+$labels['description'] = 'Kuvaus';
+$labels['all-day'] = 'koko päivä';
+$labels['export'] = 'Vie';
+$labels['exporttitle'] = 'Vie iCalendar-muotoon';
+$labels['exportrange'] = 'Tapahtumat';
+$labels['exportattachments'] = 'Liitteiden kanssa';
+$labels['customdate'] = 'Kustomoitu aika';
+$labels['location'] = 'Sijainti';
+$labels['url'] = 'Osoite';
+$labels['date'] = 'Päiväys';
+$labels['start'] = 'Alkaa';
+$labels['starttime'] = 'Aloitusaika';
+$labels['end'] = 'Päättyy';
+$labels['endtime'] = 'Päättymisaika';
+$labels['repeat'] = 'Toista';
+$labels['selectdate'] = 'Valitse päiväys';
+$labels['freebusy'] = 'Aseta tilakseni';
+$labels['free'] = 'Vapaa';
+$labels['busy'] = 'Varattu';
+$labels['outofoffice'] = 'Ei toimistolla';
+$labels['tentative'] = 'Alustava';
+$labels['mystatus'] = 'Tilani';
+$labels['status'] = 'Tila';
+$labels['status-confirmed'] = 'Vahvistettu';
+$labels['status-cancelled'] = 'Peruttu';
+$labels['priority'] = 'Tärkeys';
+$labels['sensitivity'] = 'Yksityisyys';
+$labels['public'] = 'julkinen';
+$labels['private'] = 'yksityinen';
+$labels['confidential'] = 'luottamuksellinen';
+$labels['links'] = 'Viittaus';
+$labels['alarms'] = 'Muistutus';
+$labels['comment'] = 'Kommentti';
+$labels['created'] = 'Luotu';
+$labels['changed'] = 'Viimeksi muokattu';
+$labels['unknown'] = 'Tuntematon';
+$labels['eventoptions'] = 'Valinnat';
+$labels['generated'] = 'generoitu';
+$labels['eventhistory'] = 'Historia';
+$labels['removelink'] = 'Poista sähköpostiviittaus';
+$labels['printdescriptions'] = 'Tulosta kuvaukset';
+$labels['parentcalendar'] = 'Aseta sisään';
+$labels['searchearlierdates'] = '« Etsi aiempia tapahtumia';
+$labels['searchlaterdates'] = 'Etsi myöhempiä tapahtumia »';
+$labels['andnmore'] = '$nr lisää...';
+$labels['togglerole'] = 'Klikkaa vaihtaaksesi rooli';
+$labels['createfrommail'] = 'Tallenna tapahtumana';
+$labels['importevents'] = 'Tuo tapahtumat';
+$labels['importrange'] = 'Tapahtumat';
+$labels['onemonthback'] = '1 kuukauden ajalta';
+$labels['nmonthsback'] = '$nr kuukauden ajalta';
+$labels['showurl'] = 'Näytä kalenterin osoite';
+$labels['showurldescription'] = 'Käytä seuraavia osoitteita avataksesi kalenterisi pelkässä lukumuodossa muissa sovelluksissa. Voit kopioida ja liittää osoitteen mihin tahansa iCal-muotoa tukevaan kalenterisovellukseen.';
+$labels['caldavurldescription'] = 'Kopioi tämä osoite <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a-asiakassovellukseen (esim. Evolution tai Mozilla Thunderbird) synkronoidaksesi tämän kalenterin tietokoneesi tai mobiililaitteesi kanssa.';
+$labels['findcalendars'] = 'Etsi kalentereita...';
+$labels['searchterms'] = 'Hakuehdot';
+$labels['calsearchresults'] = 'Saatavilla olevat kalenterit';
+$labels['calendarsubscribe'] = 'Luetteloi pysyvästi';
+$labels['nocalendarsfound'] = 'Kalentereita ei löytynyt';
+$labels['nrcalendarsfound'] = '$nr kalenteria löytynyt';
+$labels['quickview'] = 'Näytä vain tämä kalenteri';
+$labels['invitationspending'] = 'Odottavat kutsut';
+$labels['invitationsdeclined'] = 'Torjutut kutsut';
+$labels['changepartstat'] = 'Muuta osallistujan tilaa';
+$labels['rsvpcomment'] = 'Kutsuteksti';
+$labels['listrange'] = 'Näytettävä aikaväli';
+$labels['listsections'] = 'Jaa osiin:';
+$labels['smartsections'] = 'Älykkäät osiot';
+$labels['until'] = 'kunnes';
+$labels['today'] = 'Tänään';
+$labels['tomorrow'] = 'Huomenna';
+$labels['thisweek'] = 'Tällä viikolla';
+$labels['nextweek'] = 'Ensi viikolla';
+$labels['prevweek'] = 'Edellinen viikko';
+$labels['thismonth'] = 'Tässä kuussa';
+$labels['nextmonth'] = 'Ensi kuussa';
+$labels['weekofyear'] = 'Viikko';
+$labels['pastevents'] = 'Menneet';
+$labels['futureevents'] = 'Tulevat';
+$labels['showalarms'] = 'Näytä muistutukset';
+$labels['defaultalarmtype'] = 'Muistutuksen oletusasetus';
+$labels['defaultalarmoffset'] = 'Muistutuksen oletusaika';
+$labels['attendee'] = 'Osallistujat';
+$labels['role'] = 'Rooli';
+$labels['availability'] = 'Saatavilla';
+$labels['confirmstate'] = 'Tila';
+$labels['addattendee'] = 'Lisää osallistuja';
+$labels['roleorganizer'] = 'Järjestäjä';
+$labels['rolerequired'] = 'Vaadittu';
+$labels['roleoptional'] = 'Valinnainen';
+$labels['rolechair'] = 'Kutsuja';
+$labels['rolenonparticipant'] = 'Poissaoleva';
+$labels['cutypeindividual'] = 'Yksityishenkilö';
+$labels['cutypegroup'] = 'Ryhmä';
+$labels['cutyperesource'] = 'Resurssi';
+$labels['cutyperoom'] = 'Huone';
+$labels['availfree'] = 'Vapaa';
+$labels['availbusy'] = 'Varattu';
+$labels['availunknown'] = 'Tuntematon';
+$labels['availtentative'] = 'Alustava';
+$labels['availoutofoffice'] = 'Ei toimistolla';
+$labels['delegatedto'] = 'Delegoitu henkilölle:';
+$labels['delegatedfrom'] = 'Delegoitus henkilöltä:';
+$labels['scheduletime'] = 'Etsi saatavuus';
+$labels['sendinvitations'] = 'Lähetä kutsut';
+$labels['sendnotifications'] = 'Ilmoita osallistujille muutoksista';
+$labels['sendcancellation'] = 'Ilmoita osallistujille tapahtuman perumisesta';
+$labels['onlyworkinghours'] = 'Etsi saatavuutta työajan sisältä';
+$labels['reqallattendees'] = 'Vaadittu/kaikki osallistujat';
+$labels['prevslot'] = 'Edellinen ajankohta';
+$labels['nextslot'] = 'Seuraava ajankohta';
+$labels['suggestedslot'] = 'Ehdotettu ajankohta';
+$labels['noslotfound'] = 'Vapaata ajankohtaa ei löytynyt';
+$labels['invitationsubject'] = 'Sinut on kutsuttu tapahtumaan "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nMilloin: \$date\n\nKutsutut: \$attendees\n\nOhessa iCalendar -tiedosto mistä löytyvät kaikki tapahtuman yksityistiedot. Voit tuoda tämän tiedoston kalenteriohjelmaasi.";
+$labels['invitationattendlinks'] = "Mikäli sähköpostiohjelmasi ei tue iTip pyyntöjä, voit aina käyttää ao. osoitetta kutsun hyväksymiseen / hylkäämiseen:\n\$url";
+$labels['eventupdatesubject'] = '"$title" on päivitetty';
+$labels['eventupdatesubjectempty'] = 'Sinua koskeva tapahtuma on päivitetty';
+$labels['eventupdatemailbody'] = "*\$title*\n\nMilloin: \$date\n\nKutsutut: \$attendees\n\nOhessa iCalendar -tiedosto mistä löytyvät kaikki päivitetyn tapahtuman yksityistiedot. Voit tuoda tämän tiedoston kalenteriohjelmaasi.";
+$labels['eventcancelsubject'] = '"$title" on peruttu';
+$labels['eventcancelmailbody'] = "*\$title*\n\nMilloin: \$date\n\nKutsutut: \$attendees\n\nTämä tapahtuma on peruttu \$organizer toimesta.\n\nLöydät liitteenä iCalendar -tiedoston tapahtuman päivitetyin tiedoin.";
+$labels['itipobjectnotfound'] = 'Viestissä mainittua tapahtumaa ei löydy kalenteristasi.';
+$labels['itipmailbodyaccepted'] = "\$sender on hyväksynyt kutsun seuraavaan tapahtumaan:\n\n*\$title*\n\nMilloin: \$date\n\nKutsutut: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender on alustavasti hyväksynyt kutsun seuraavaan tapahtumaan:\n\n*\$title*\n\nMilloin: \$date\n\nKutsutut: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender on hylännyt kutsun seuraavaan tapahtumaan:\n\n*\$title*\n\nMilloin: \$date\n\nKutsutut: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender on hylännyt osallistumisesi tapahtumaan:\n\n*\$title*\n\nMilloin: \$date";
+$labels['itipmailbodydelegated'] = "\$sender on delegoinut osallistumisensa seuraavaan tapahtumaan:\n\n*\$title*\n\nAjankohta: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender on delegoinut osallistumisensa sinulle seuraavaan tapahtumaan:\n\n*\$title*\n\nAjankohta: \$date";
+$labels['itipdeclineevent'] = 'Haluatko perua kutsun tähän tapahtumaan?';
+$labels['declinedeleteconfirm'] = 'Haluatko poistaa tämän hylätyn tapahtuman kalenteristasi?';
+$labels['itipcomment'] = 'Kutsun/herätteen kommentit';
+$labels['itipcommenttitle'] = 'Tämä kommentti liitetään osallistujille lähetettävään kutsuun/heräteviestiin';
+$labels['notanattendee'] = 'Sinua ei ole määritetty tapahtuman osanottajaksi';
+$labels['eventcancelled'] = 'Tapahtuma on peruttu';
+$labels['updatemycopy'] = 'Päivitä kalenterini';
+$labels['savetocalendar'] = 'Tallenna kalenteriin';
+$labels['openpreview'] = 'Tarkista kalenteri';
+$labels['noearlierevents'] = 'Ei aiempia tapahtumia';
+$labels['nolaterevents'] = 'Ei myöhempiä tapahtumia';
+$labels['resource'] = 'Resurssi';
+$labels['addresource'] = 'Varaa resurssi';
+$labels['findresources'] = 'Etsi resursseja';
+$labels['resourcedetails'] = 'Tiedot';
+$labels['resourceavailability'] = 'Saatavuus';
+$labels['resourceowner'] = 'Omistaja';
+$labels['resourceadded'] = 'Resurssi on liitetty tapahtumaasi';
+$labels['tabsummary'] = 'Yhteenveto';
+$labels['tabrecurrence'] = 'Toistuminen';
+$labels['tabattendees'] = 'Osallistujat';
+$labels['tabresources'] = 'Resurssit';
+$labels['tabattachments'] = 'Liitteet';
+$labels['tabsharing'] = 'Jakaminen';
+$labels['deleteobjectconfirm'] = 'Haluatko varmasti poistaa tämän tapahtuman?';
+$labels['deleteventconfirm'] = 'Haluatko varmasti poistaa tämän tapahtuman?';
+$labels['deletecalendarconfirm'] = 'Haluatko varmasti poistaa tämän kalenterin ja kaikki sen tapahtumat?';
+$labels['deletecalendarconfirmrecursive'] = 'Haluatko varmasti poistaa tämän kalenterin ja kaikki sen tapahtumat sekä alikalenterit?';
+$labels['savingdata'] = 'Tallennetaan tietoja...';
+$labels['errorsaving'] = 'Muutosten tallentaminen epäonnistui.';
+$labels['operationfailed'] = 'Pyydetty toiminto epäonnistui.';
+$labels['invalideventdates'] = 'Annettu virheellisiä päivämääriä! Tarkista syöte.';
+$labels['invalidcalendarproperties'] = 'Kalenterin ominaisuudet ovat virheelliset. Aseta kelvollinen nimi.';
+$labels['searchnoresults'] = 'Valituista kalentereista ei löytynyt tapahtumia.';
+$labels['successremoval'] = 'Tapahtuma on poistettu onnistuneesti.';
+$labels['successrestore'] = 'Tapahtuma on palautettu onnistuneesti.';
+$labels['errornotifying'] = 'Herätteen lähetys osallistujille epäonnistui';
+$labels['errorimportingevent'] = 'Tapahtuman tuonti epäonnistui';
+$labels['importwarningexists'] = 'Tämän tapahtuman kopio löytyy jo kalenteristasi';
+$labels['newerversionexists'] = 'Uudempi versio tästä tapahtumasta on jo olemassa! Keskeytetty.';
+$labels['nowritecalendarfound'] = 'Tapahtuman tallentamiseksi ei löytynyt kalenteria';
+$labels['importedsuccessfully'] = 'Tapahtuma lisättiin onnistuneesti kalenteriin \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Tapahtuma on onnistuneesti päivitetty kalenterissa \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Osallistujan tila päivitetty onnistuneesti';
+$labels['itipsendsuccess'] = 'Kutsu lähetetty osallistujille.';
+$labels['itipresponseerror'] = 'Vastauksen lähettäminen tapahtumakutsuun epäonnistui';
+$labels['itipinvalidrequest'] = 'Kutsu ei ole enää kelvollinen';
+$labels['sentresponseto'] = 'Vastaus kutsuun lähetetty onnistuneesti sähköpostiin $mailto';
+$labels['localchangeswarning'] = 'Olet tekemässä muutoksia, jotka vaikuttavat ainoastaan omaan kalenteriisi. Muutoksia ei lähetetä tapahtuman järjestäjälle.';
+$labels['importsuccess'] = '$nr tapahtumaa tuotiin onnistuneesti';
+$labels['importnone'] = 'Tuotavaksi tarkoitettuja tapahtumia ei löytynyt';
+$labels['importerror'] = 'Tuotaessa tapahtui virhe';
+$labels['aclnorights'] = 'Sinulla ei ole ylläpitäjän oikeuksia tähän kalenteriin.';
+$labels['changeeventconfirm'] = 'Vaihda tapahtuma';
+$labels['removeeventconfirm'] = 'Poista tapahtuma';
+$labels['changerecurringeventwarning'] = 'Tämä on tuistuva tapahtuma. Haluatko muokata vain tätä ajankohtaa, tätä ja tulevia tapahtuman ajankohtia, kaikkia tapahtuman ajankohtia vai tallentaa kokonaan uutena tapahtumana? ';
+$labels['removerecurringeventwarning'] = 'Tämä on toistuva tapahtuma. Haluatko poistaa vain nykyisen tapahtuman, nykyisen tapahtuman ja tulevaisuuden tapahtumat vai kaikki tapahtumaan liittyvät merkinnät?';
+$labels['removerecurringallonly'] = 'Tämä on toistuva tapahtuma. Osallistujana voit poistaa vain koko tapahtuman kaikkine toistumiskertoineen.';
+$labels['currentevent'] = 'Nykyinen';
+$labels['futurevents'] = 'Tulevat';
+$labels['allevents'] = 'Kaikki';
+$labels['saveasnew'] = 'Tallenna uutena';
+$labels['birthdays'] = 'Syntymäpäivät';
+$labels['birthdayscalendar'] = 'Syntymäpäivät kalenteri';
+$labels['displaybirthdayscalendar'] = 'Näytä syntymäpäivät kalenterissa';
+$labels['birthdayscalendarsources'] = 'Näistä osoitekirjoista';
+$labels['birthdayeventtitle'] = 'Syntymäpäivä: $name';
+$labels['birthdayage'] = 'Ikä $age';
+$labels['objectchangelog'] = 'Muuta historiaa';
+$labels['objectdiff'] = 'Muutokset versiosta $rev1 versioon $rev2';
+$labels['revision'] = 'Versio';
+$labels['user'] = 'Käyttäjä';
+$labels['operation'] = 'Toiminto';
+$labels['actionappend'] = 'Tallennettu';
+$labels['actionmove'] = 'Siirretty';
+$labels['actiondelete'] = 'Poistettu';
+$labels['compare'] = 'Vertaa';
+$labels['showrevision'] = 'Näytä tämä versio';
+$labels['restore'] = 'Palauta tämä versio';
+$labels['objectnotfound'] = 'Tapahtumadatan lataus epäonnistui';
+$labels['objectchangelognotavailable'] = 'Tapahtuman muutoshistoria ei ole saatavilla';
+$labels['objectdiffnotavailable'] = 'Vertailu ei ole saatavilla valittujen versioiden välillä';
+$labels['revisionrestoreconfirm'] = 'Haluatko varmasti palauttaa tämän tapahtuman version $rev? Nykyinen tapahtuma korvataan vanhalla versiolla.';
+$labels['objectrestoresuccess'] = 'Versio $rev palautettiin onnistuneesti';
+$labels['objectrestoreerror'] = 'Vanhan version palauttaminen epäonnistui';
+$labels['arialabelminical'] = 'Kalenterin ajankohdan valinta';
+$labels['arialabelcalendarview'] = 'Kalenterinäkymä';
+$labels['arialabelsearchform'] = 'Tapahtumahaun lomake';
+$labels['arialabelquicksearchbox'] = 'Tapatumahaun syöte';
+$labels['arialabelcalsearchform'] = 'Kalenterihakujen lomake';
+$labels['calendaractions'] = 'Kalenterin toiminnot';
+$labels['arialabeleventattendees'] = 'Tapahtuman osallistujalista';
+$labels['arialabeleventresources'] = 'Tapahtuman resurssilista';
+$labels['arialabelresourcesearchform'] = 'Resurssien hakulomake';
+$labels['arialabelresourceselection'] = 'Saatavilla olevat resurssit';
+?>
diff --git a/calendar/localization/fr_FR.inc b/calendar/localization/fr_FR.inc
new file mode 100644
index 0000000..1c89721
--- /dev/null
+++ b/calendar/localization/fr_FR.inc
@@ -0,0 +1,271 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Vue par défaut';
+$labels['time_format'] = 'Format de l\'heure';
+$labels['timeslots'] = 'Créneau horaire';
+$labels['first_day'] = 'Premier jour de la semaine';
+$labels['first_hour'] = 'Première heure à afficher';
+$labels['workinghours'] = 'Heures de travail';
+$labels['add_category'] = 'Ajouter une catégorie';
+$labels['remove_category'] = 'Supprimer une catégorie';
+$labels['defaultcalendar'] = 'Ajouter un nouvel évènement';
+$labels['eventcoloring'] = 'Couleurs des évènements';
+$labels['coloringmode0'] = 'Selon l\'agenda';
+$labels['coloringmode1'] = 'Selon la catégorie';
+$labels['coloringmode2'] = 'Calendrier en contour, catégorie en contenu';
+$labels['coloringmode3'] = 'Catégorie en contour, calendrier en contenu';
+$labels['afternothing'] = 'Ne rien faire';
+$labels['aftertrash'] = 'Déplacer dans la corbeille';
+$labels['afterdelete'] = 'Supprimer ce message';
+$labels['afterflagdeleted'] = 'Marquer comme supprimer';
+$labels['aftermoveto'] = 'Déplacer vers...';
+$labels['itipoptions'] = 'Invitations à l\'évenement';
+$labels['afteraction'] = 'Après une invitation ou une modification, le message est traité';
+$labels['calendar'] = 'Agenda';
+$labels['calendars'] = 'Agendas';
+$labels['category'] = 'Catégorie';
+$labels['categories'] = 'Catégories';
+$labels['createcalendar'] = 'Créer un nouvel agenda';
+$labels['editcalendar'] = 'Modifier les propriétés de l\'agenda';
+$labels['name'] = 'Nom';
+$labels['color'] = 'Couleur';
+$labels['day'] = 'Jour';
+$labels['week'] = 'Semaine';
+$labels['month'] = 'Mois';
+$labels['agenda'] = 'Ordre du jour';
+$labels['new'] = 'Nouveau';
+$labels['new_event'] = 'Nouvel évènement';
+$labels['edit_event'] = 'Modifier l\'évènement';
+$labels['edit'] = 'Modifier';
+$labels['save'] = 'Enregistrer';
+$labels['removelist'] = 'supprimer de la liste';
+$labels['cancel'] = 'Annuler';
+$labels['select'] = 'Sélectionner';
+$labels['print'] = 'Imprimer';
+$labels['printtitle'] = 'Imprimer les agendas';
+$labels['title'] = 'Résumé';
+$labels['description'] = 'Description';
+$labels['all-day'] = 'Toute la journée';
+$labels['export'] = 'Exporter';
+$labels['exporttitle'] = 'Exporter vers iCalendar';
+$labels['exportrange'] = 'Évènements depuis';
+$labels['exportattachments'] = 'With attachments';
+$labels['customdate'] = 'Date personnalisée';
+$labels['location'] = 'Lieu';
+$labels['url'] = 'URL';
+$labels['date'] = 'Date';
+$labels['start'] = 'Début';
+$labels['starttime'] = 'Début';
+$labels['end'] = 'Fin';
+$labels['endtime'] = 'Fin';
+$labels['repeat'] = 'Répéter';
+$labels['selectdate'] = 'Sélectionner une date';
+$labels['freebusy'] = 'Montrez moi comme';
+$labels['free'] = 'Libre';
+$labels['busy'] = 'Occupé';
+$labels['outofoffice'] = 'Absent';
+$labels['tentative'] = 'Provisoire';
+$labels['mystatus'] = 'Mon status';
+$labels['status'] = 'Statut';
+$labels['status-confirmed'] = 'Confirmé';
+$labels['status-cancelled'] = 'Annulée';
+$labels['priority'] = 'Priorité';
+$labels['sensitivity'] = 'Diffusion';
+$labels['public'] = 'publique';
+$labels['private'] = 'privée';
+$labels['confidential'] = 'Confidentiel';
+$labels['links'] = 'Référence';
+$labels['alarms'] = 'Rappel';
+$labels['comment'] = 'Commentaire';
+$labels['created'] = 'Créée';
+$labels['changed'] = 'Dernière modification';
+$labels['unknown'] = 'Inconnu';
+$labels['eventoptions'] = 'Options';
+$labels['generated'] = 'généré à';
+$labels['eventhistory'] = 'Historique';
+$labels['removelink'] = 'Enlever référence d\'e-mail';
+$labels['printdescriptions'] = 'Imprimer les descriptions';
+$labels['parentcalendar'] = 'Ajouter à l\'intérieur';
+$labels['searchearlierdates'] = '« Chercher des évènements plus ancien';
+$labels['searchlaterdates'] = 'Chercher des évènement plus récent »';
+$labels['andnmore'] = '$nr de plus...';
+$labels['togglerole'] = 'Cliquez pour changer de rôle';
+$labels['createfrommail'] = 'Enregistrer comme un évènement';
+$labels['importevents'] = 'Importer des évènements';
+$labels['importrange'] = 'Évènements depuis';
+$labels['onemonthback'] = '1 mois précédent';
+$labels['nmonthsback'] = '$nr mois précédents';
+$labels['showurl'] = 'Afficher l\'URL de l\'agenda';
+$labels['showurldescription'] = 'Utilisez l\'adresse suivante pour accéder(lecture seule) à votre agenda depuis une autre application. Vous pouvez copier/coller celle-ci dans n\'importe quel agenda électronique gérant le format iCal.';
+$labels['caldavurldescription'] = 'Copiez cette adresse vers une application client (comme Evolution ou Mozilla Thunderbird) compatible <a href="http://fr.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> pour synchroniser ce calendrier avec votre ordinateur ou votre smartphone.';
+$labels['findcalendars'] = 'Recherche de calendriers...';
+$labels['searchterms'] = 'Critères de recherche';
+$labels['calsearchresults'] = 'Calendriers disponibles';
+$labels['calendarsubscribe'] = 'Lister définitivement';
+$labels['nocalendarsfound'] = 'Aucun calendriers trouvés';
+$labels['nrcalendarsfound'] = '$nr calendriers trouvés';
+$labels['quickview'] = 'Voir uniquement ce calendrier';
+$labels['invitationspending'] = 'Invitations en attente';
+$labels['invitationsdeclined'] = 'Invitations refusées';
+$labels['changepartstat'] = 'Changer le statut du participent';
+$labels['rsvpcomment'] = 'Texte d\'invitation';
+$labels['listrange'] = 'Intervalle à afficher :';
+$labels['listsections'] = 'Diviser en :';
+$labels['smartsections'] = 'Section intelligente';
+$labels['until'] = 'jusqu\'à';
+$labels['today'] = 'Aujourd\'hui';
+$labels['tomorrow'] = 'Demain';
+$labels['thisweek'] = 'Cette semaine';
+$labels['nextweek'] = 'Semaine prochaine';
+$labels['prevweek'] = 'Semaine précédente';
+$labels['thismonth'] = 'Ce mois';
+$labels['nextmonth'] = 'Mois prochain';
+$labels['weekofyear'] = 'Semaine';
+$labels['pastevents'] = 'Passé';
+$labels['futureevents'] = 'Futur';
+$labels['showalarms'] = 'Afficher les rappels';
+$labels['defaultalarmtype'] = 'Paramètre de rappel par défaut';
+$labels['defaultalarmoffset'] = 'Durée de rappel par défaut';
+$labels['attendee'] = 'Participant';
+$labels['role'] = 'Rôle';
+$labels['availability'] = 'Dispo.';
+$labels['confirmstate'] = 'Statut';
+$labels['addattendee'] = 'Ajouter participant';
+$labels['roleorganizer'] = 'Organisateur';
+$labels['rolerequired'] = 'Requis';
+$labels['roleoptional'] = 'Optionel';
+$labels['rolechair'] = 'Chair';
+$labels['rolenonparticipant'] = 'Absent';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Groupe';
+$labels['cutyperesource'] = 'Ressource';
+$labels['cutyperoom'] = 'Salle';
+$labels['availfree'] = 'Disponible';
+$labels['availbusy'] = 'Occupé';
+$labels['availunknown'] = 'Inconnu';
+$labels['availtentative'] = 'Provisoire';
+$labels['availoutofoffice'] = 'Absent';
+$labels['delegatedto'] = 'Délégué à :';
+$labels['delegatedfrom'] = 'Délégué de :';
+$labels['scheduletime'] = 'Trouver les disponibilités';
+$labels['sendinvitations'] = 'Envoyer les invitations';
+$labels['sendnotifications'] = 'Informer les participants des modifications';
+$labels['sendcancellation'] = 'Informer les participants de l\'annulation';
+$labels['onlyworkinghours'] = 'Trouver des disponibilités en fonction de mes heures de travail';
+$labels['reqallattendees'] = 'Demandé/tous';
+$labels['prevslot'] = 'Créneau précédent';
+$labels['nextslot'] = 'Créneau suivant';
+$labels['suggestedslot'] = 'Emplacement suggéré';
+$labels['noslotfound'] = 'Impossible de trouver un créneau disponible';
+$labels['invitationsubject'] = 'Vous avez été invité à "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nQuand: \$date\n\nParticipants: \$attendees\n\nVous trouverez ci-joint un fichier iCalendar avec tous les détails de l'évènement que vous pourrez importer dans votre agenda électronique.";
+$labels['invitationattendlinks'] = "Dans le cas où votre application de messagerie ne gère pas les demandes \"iTip\". Vous pouvez utiliser ce lien pour accepter ou refuser l'invitation : \n\$url";
+$labels['eventupdatesubject'] = '"$title" a été modifié';
+$labels['eventupdatesubjectempty'] = 'Un évènement vous concernant a été modifié';
+$labels['eventupdatemailbody'] = "*\$title*\n\nQuand: \$date\n\nParticipants: \$attendees\n\nVous trouverez ci-joint un fichier iCalendar avec tous les modifications de l'évènement que vous pourrez importer dans votre agenda électronique.";
+$labels['eventcancelsubject'] = '"$title" a été annulé';
+$labels['eventcancelmailbody'] = "*\$title*\n\nQuand: \$date\n\nParticipants: \$attendees\n\nL'évènement a été annulé par \$organizer.\n\nVous trouverez en pièce jointe un fichier iCalendar avec les modifications de l'évènement que vous pourrez importer dans votre agenda électronique.";
+$labels['itipobjectnotfound'] = 'L\'évènement lié à ce message n\'a pas été trouvé dans votre calendrier.';
+$labels['itipmailbodyaccepted'] = "\$sender a accepté l'invitation à l'évènement suivant :\n\n*\$title*\n\nQuand: \$date\n\nParticipants: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender a accepté provisoirement l'invitation à l'évènement suivant :\n\n*\$title*\n\nQuand: \$date\n\nParticipants: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender a refusé l'invitation à l'évènement suivant :\n\n*\$title*\n\nQuand: \$date\n\nParticipants: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender a rejeté votre participation à l’évènement suivant :\n\n*\$title*\n\nLe: \$date";
+$labels['itipdeclineevent'] = 'Voulez-vous refuser l\'invitation à cet évènement?';
+$labels['declinedeleteconfirm'] = 'Voulez-vous aussi supprimer cet évènement annulé, de votre calendrier ?';
+$labels['itipcomment'] = 'Commentaire d’invitation ou de notification';
+$labels['itipcommenttitle'] = 'Ce commentaire sera inséré dans le message d\'invitation ou de notification envoyé aux participants';
+$labels['notanattendee'] = 'Vous n\'êtes pas dans la liste des participants à cet évènement';
+$labels['eventcancelled'] = 'L\'évènement a été annulé';
+$labels['saveincalendar'] = 'Enregistrer sous';
+$labels['updatemycopy'] = 'Mise à jour dans mon calendrier';
+$labels['savetocalendar'] = 'Sauvegarde dans le calendrier';
+$labels['openpreview'] = 'Test calendrier';
+$labels['noearlierevents'] = 'Aucun évènements passé';
+$labels['nolaterevents'] = 'Aucun évènement futur';
+$labels['resource'] = 'Ressource';
+$labels['addresource'] = 'Carnet de ressources';
+$labels['findresources'] = 'Recherche des ressources';
+$labels['resourcedetails'] = 'Détails';
+$labels['resourceavailability'] = 'Disponibilité';
+$labels['resourceowner'] = 'Propriétaire';
+$labels['resourceadded'] = 'Cette ressource a été ajouté à l\'évènement';
+$labels['tabsummary'] = 'Résumé';
+$labels['tabrecurrence'] = 'Récurrence';
+$labels['tabattendees'] = 'Participants';
+$labels['tabresources'] = 'Ressources';
+$labels['tabattachments'] = 'Pièces jointes';
+$labels['tabsharing'] = 'Partage';
+$labels['deleteobjectconfirm'] = 'Voulez-vous vraiment supprimer cet évènement?';
+$labels['deleteventconfirm'] = 'Voulez-vous vraiment supprimer cet évènement?';
+$labels['deletecalendarconfirm'] = 'Voulez-vous vraiment supprimer cet agenda et tous ses évènements?';
+$labels['deletecalendarconfirmrecursive'] = 'Voulez-vous vraiment supprimer ce calendrier avec tous ces évènements et ces sous calendriers ?';
+$labels['savingdata'] = 'Enregistrer...';
+$labels['errorsaving'] = 'Échec lors de l\'enregistrement des changements';
+$labels['operationfailed'] = 'L\'opération demandée a échoué';
+$labels['invalideventdates'] = 'Dates invalides! Veuillez vérifier votre saisie.';
+$labels['invalidcalendarproperties'] = 'Propriétés d\'agenda invalides! Veuillez saisir un nom valide.';
+$labels['searchnoresults'] = 'Pas d\'évènement trouvé dans les agendas sélectionnés.';
+$labels['successremoval'] = 'L\'évènement a été supprimé.';
+$labels['successrestore'] = 'L\'évènement a été restauré.';
+$labels['errornotifying'] = 'Échec de l\'envoi de notification aux participants ';
+$labels['errorimportingevent'] = 'Échec de l\'import de l\'évènement';
+$labels['importwarningexists'] = 'Une copie de cet évènement existe déjà dans votre calendrier.';
+$labels['newerversionexists'] = 'Une nouvelle version de cet évènement existe! Abandon.';
+$labels['nowritecalendarfound'] = 'Pas d\'agenda trouvé pour enregistrer l\'évènement';
+$labels['importedsuccessfully'] = 'L\'évènement a été ajouté à l\'agenda \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Cet évènement a été modifié avec succès dans \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Le statut des participants a été modifié';
+$labels['itipsendsuccess'] = 'Invitation envoyé aux participants.';
+$labels['itipresponseerror'] = 'Échec de l\'envoi d\'une réponse à cette invitation.';
+$labels['itipinvalidrequest'] = 'C\'est invitation n\'est plus valide.';
+$labels['sentresponseto'] = 'La réponse à l\'invitation a été envoyé à $mailto';
+$labels['localchangeswarning'] = 'Vous êtes sur le point d\'effectuer des modifications qui seront effectifs sur votre calendrier mais qui ne seront pas envoyés à l\'organisateur de l’évènement.';
+$labels['importsuccess'] = '$nr évènements importés.';
+$labels['importnone'] = 'Pas d\'évènements à importer';
+$labels['importerror'] = 'Une erreur est arrivée lors de l\'import';
+$labels['aclnorights'] = 'Vous n\'avez pas les droits d\'administration sur cet agenda.';
+$labels['changeeventconfirm'] = 'Modifier l\'évènement';
+$labels['removeeventconfirm'] = 'Supprimer l\'évènement';
+$labels['changerecurringeventwarning'] = 'Ceci est un évènement récurant. Voulez vous éditer seulement cette occurrence, celle-ci et toutes les suivantes, toutes les occurrences ou l\'enregistrer comme un nouvel évènement? ';
+$labels['removerecurringeventwarning'] = 'Ceci est un évènement recrurent. Voulez-vous supprimer uniquement l\'évènement courent, l’évènement courent et toutes ces occurrences futures ou toutes les occurrences ?';
+$labels['currentevent'] = 'Cette occurrence';
+$labels['futurevents'] = 'Cette occurrence et toutes les suivantes';
+$labels['allevents'] = 'Toutes les occurrences';
+$labels['saveasnew'] = 'Enregistrer comme un nouvel évènement';
+$labels['birthdays'] = 'Anniversaires';
+$labels['birthdayscalendar'] = 'Calendrier des anniversaires';
+$labels['displaybirthdayscalendar'] = 'Afficher le calendrier des anniversaires';
+$labels['birthdayscalendarsources'] = 'Depuis ces carnets d\'adresses';
+$labels['birthdayeventtitle'] = 'Anniversaire de $name';
+$labels['birthdayage'] = 'Age $age';
+$labels['objectchangelog'] = 'Historique des modifications';
+$labels['revision'] = 'Version';
+$labels['user'] = 'Utilisateur';
+$labels['operation'] = 'Action';
+$labels['actionappend'] = 'Sauvegardé';
+$labels['actionmove'] = 'Déplacé';
+$labels['actiondelete'] = 'Supprimé';
+$labels['compare'] = 'Comparer';
+$labels['showrevision'] = 'Afficher cette version';
+$labels['restore'] = 'Restaurer cette version';
+$labels['objectnotfound'] = 'Impossible de charger les données de l’évènement';
+$labels['objectchangelognotavailable'] = 'Il n\'y a pas d\'historique des modifications pour cet évènement';
+$labels['objectdiffnotavailable'] = 'La comparaison des versions sélectionnées est impossible';
+$labels['revisionrestoreconfirm'] = 'Voulez-vous vraiment restaurer le version $rev de cet évènement ? Cette action va remplacer l\'évènement courent par l\'ancienne version.';
+$labels['arialabelminical'] = 'Sélection de la date du calendrier';
+$labels['arialabelcalendarview'] = 'Vue du calendrier';
+$labels['arialabelsearchform'] = 'Recherche d\'évènements depuis';
+$labels['arialabelquicksearchbox'] = 'Saisie de le recherche d\'évènements';
+$labels['arialabelcalsearchform'] = 'Recherche de calendriers';
+$labels['calendaractions'] = 'Actions calendrier';
+$labels['arialabeleventattendees'] = 'Liste des participants à l\'évènement';
+$labels['arialabeleventresources'] = 'Liste des ressources de l\'évènement';
+$labels['arialabelresourcesearchform'] = 'Recherche des ressources';
+$labels['arialabelresourceselection'] = 'Ressources disponibles';
+?>
diff --git a/calendar/localization/hu_HU.inc b/calendar/localization/hu_HU.inc
new file mode 100644
index 0000000..3945e82
--- /dev/null
+++ b/calendar/localization/hu_HU.inc
@@ -0,0 +1,216 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Alapnézett nézet';
+$labels['time_format'] = 'Idő formátum';
+$labels['timeslots'] = 'Órák felbontása időrésekre';
+$labels['first_day'] = 'A hét első napja';
+$labels['first_hour'] = 'Kezdő óra';
+$labels['workinghours'] = 'Munkaidő';
+$labels['add_category'] = 'Kategória hozzáadása';
+$labels['remove_category'] = 'Kategória törlése';
+$labels['defaultcalendar'] = 'Új események alapértelmezett helye';
+$labels['eventcoloring'] = 'Események színezése';
+$labels['coloringmode0'] = 'Naptár szerint';
+$labels['coloringmode1'] = 'Kategória szerint';
+$labels['coloringmode2'] = 'Naptár színe körvonal, kategória színe belsőrész';
+$labels['coloringmode3'] = 'Kategória színe körvonal, naptár színe belsőrész';
+$labels['calendar'] = 'Naptár';
+$labels['calendars'] = 'Naptárak';
+$labels['category'] = 'Kategória';
+$labels['categories'] = 'Kategóriák';
+$labels['createcalendar'] = 'Új naptár létrehozása';
+$labels['editcalendar'] = 'Naptár tulajdonságai';
+$labels['name'] = 'Név';
+$labels['color'] = 'Szín';
+$labels['day'] = 'Nap';
+$labels['week'] = 'Hét';
+$labels['month'] = 'Hónap';
+$labels['agenda'] = 'Napirend';
+$labels['new'] = 'Új';
+$labels['new_event'] = 'Új esemény';
+$labels['edit_event'] = 'Esemény módosítása';
+$labels['edit'] = 'Módosítás';
+$labels['save'] = 'Mentés';
+$labels['removelist'] = 'Remove from list';
+$labels['cancel'] = 'Mégse';
+$labels['select'] = 'Jelölés';
+$labels['print'] = 'Nyomtatás';
+$labels['printtitle'] = 'Naptárak nyomtatása';
+$labels['title'] = 'Tárgy';
+$labels['description'] = 'Leírás';
+$labels['all-day'] = 'Egész nap';
+$labels['export'] = 'Exportálás';
+$labels['exporttitle'] = 'Exportálás iCalendar-ba';
+$labels['exportrange'] = 'Visszamenőleg';
+$labels['exportattachments'] = 'Csatolmányokkal';
+$labels['customdate'] = 'Megadott dátumig';
+$labels['location'] = 'Hol';
+$labels['url'] = 'URL';
+$labels['date'] = 'Dátum';
+$labels['start'] = 'Kezdet';
+$labels['starttime'] = 'Start time';
+$labels['end'] = 'Vég';
+$labels['repeat'] = 'Ismétlődés';
+$labels['selectdate'] = 'Válasszon dátumot';
+$labels['freebusy'] = 'Foglaltság';
+$labels['free'] = 'Szabad';
+$labels['busy'] = 'Foglalt';
+$labels['outofoffice'] = 'Házon kívűl';
+$labels['tentative'] = 'Feltételes';
+$labels['status'] = 'Stát.';
+$labels['status-confirmed'] = 'Confirmed';
+$labels['status-cancelled'] = 'Cancelled';
+$labels['priority'] = 'Prioritás';
+$labels['sensitivity'] = 'Manánszféra';
+$labels['public'] = 'publikus';
+$labels['private'] = 'privát';
+$labels['confidential'] = 'titkos';
+$labels['links'] = 'Reference';
+$labels['alarms'] = 'Emlékeztető';
+$labels['comment'] = 'Megjegyzés';
+$labels['created'] = 'Created';
+$labels['changed'] = 'Last Modified';
+$labels['unknown'] = 'Ismeretlen foglaltság';
+$labels['generated'] = 'készítve:';
+$labels['removelink'] = 'Remove email reference';
+$labels['printdescriptions'] = 'Leírás nyomtatása';
+$labels['parentcalendar'] = 'Szülőnaptár';
+$labels['searchearlierdates'] = '< Korábbi események keresése';
+$labels['searchlaterdates'] = 'Későbbi események keresése >';
+$labels['andnmore'] = 'még $nr ...';
+$labels['togglerole'] = 'Kattintson a szerepváltáshoz';
+$labels['createfrommail'] = 'Mentés naptári eseményként';
+$labels['importevents'] = 'Események importálása';
+$labels['importrange'] = 'Visszamenőleg';
+$labels['onemonthback'] = '1 hónapra';
+$labels['nmonthsback'] = '$nr hónapra';
+$labels['showurl'] = 'Naptár URL címe';
+$labels['showurldescription'] = 'Ezen a címen érhető el (csak olvasásra!) a naptár más alkalmazások számára, iCal formátumban.';
+$labels['caldavurldescription'] = 'Másolja be ezt a címet egy <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a>-kompatibilis kliensbe (pl. Evolution vagy Mozilla Thunderbird) hogy kétirányú szinkronizációval tudjon a naptárához hozzáférni.';
+$labels['searchterms'] = 'Search terms';
+$labels['calendarsubscribe'] = 'List permanently';
+$labels['listrange'] = 'Megjelenítés:';
+$labels['listsections'] = 'Felosztás:';
+$labels['smartsections'] = 'Áttekintve';
+$labels['until'] = 'eddig';
+$labels['today'] = 'Ma';
+$labels['tomorrow'] = 'Holnap';
+$labels['thisweek'] = 'Ezen a héten';
+$labels['nextweek'] = 'Jövő héten';
+$labels['thismonth'] = 'Ebben a hónapban';
+$labels['nextmonth'] = 'Jövő hónapban';
+$labels['weekofyear'] = 'Hét:';
+$labels['pastevents'] = 'Múltban';
+$labels['futureevents'] = 'Jövőben';
+$labels['showalarms'] = 'Emlékeztetők megjelenítése';
+$labels['defaultalarmtype'] = 'Alapértelmezett emlékeztető';
+$labels['defaultalarmoffset'] = 'Alapértelmezett emlékeztető ideje';
+$labels['attendee'] = 'Résztvevő';
+$labels['role'] = 'Szerepkör';
+$labels['availability'] = 'Elérh.';
+$labels['confirmstate'] = 'Stát.';
+$labels['addattendee'] = 'Résztvevő meghívása';
+$labels['roleorganizer'] = 'Szervező';
+$labels['rolerequired'] = 'Kötelező';
+$labels['roleoptional'] = 'Opcionális';
+$labels['rolechair'] = 'Elnöklő';
+$labels['rolenonparticipant'] = 'Hiányzó';
+$labels['cutypeindividual'] = 'Egyén';
+$labels['cutypegroup'] = 'Csoport';
+$labels['cutyperesource'] = 'Eszköz';
+$labels['cutyperoom'] = 'Helyiség';
+$labels['availfree'] = 'Szabad';
+$labels['availbusy'] = 'Foglalt';
+$labels['availunknown'] = 'Ismeretlen foglaltság';
+$labels['availtentative'] = 'Feltételes';
+$labels['availoutofoffice'] = 'Házon kívül';
+$labels['delegatedto'] = 'Beosztva ide: ';
+$labels['delegatedfrom'] = 'Beosztva innen: ';
+$labels['scheduletime'] = 'Elérhetőség';
+$labels['sendinvitations'] = 'Meghívók küldése';
+$labels['sendnotifications'] = 'Résztvevők értesítése a változásokról';
+$labels['sendcancellation'] = 'Résztvevők értesítése a lemondásról';
+$labels['onlyworkinghours'] = 'Elérhetőség figyelembevétele csak az én munkaidőmben';
+$labels['reqallattendees'] = 'Mindenki részére';
+$labels['prevslot'] = 'Előző idősáv';
+$labels['nextslot'] = 'Következő idősáv';
+$labels['noslotfound'] = 'Nem sikerült szabad idősávot találni';
+$labels['invitationsubject'] = '$title';
+$labels['invitationmailbody'] = "Meghívó érkezett '\$title' eseményre.\n\nIdőpont: \$date\nSzervező: \$organizer\nRésztvevők: \$attendees\n\n\nMellékletben egy iCalendar naptárbejegyzés, mely tetszőleges naptárprogramba importálható.";
+$labels['invitationattendlinks'] = "Amennyiben a levelezőben nem lát elfogadó/elutasító gombokat (a levelező program nem támogatja az iTip üzeneteket), kattintson ide a meghívó elfogadására, vagy elutasítására:\n\$url";
+$labels['eventupdatesubject'] = '$title - módosítva';
+$labels['eventupdatesubjectempty'] = 'Egy Önt érintő esemény módosítva lett';
+$labels['eventupdatemailbody'] = "Módosítás érkezett '\$title' eseményre vonatkozóan.\n\nIdőpont: \$date\nSzervező: \$organizer\nRésztvevők: \$attendees\n\n\nMellékletben egy frissített iCalendar naptárbejegyzés, mely tetszőleges naptárprogramba importálható.";
+$labels['eventcancelsubject'] = '$title - lemondva';
+$labels['eventcancelmailbody'] = "'\$title' eseményre \$organizer szervező visszavonta a meghívást.\n\nIdőpont: \$date\nRésztvevők: \$attendees\n\n\nMellékletben egy frissített iCalendar naptárbejegyzés, mely tetszőleges naptárprogramba importálható.";
+$labels['itipobjectnotfound'] = 'Az üzenetben hivatkozott esemény nem található a naptárban.';
+$labels['itipmailbodyaccepted'] = "\$sender elfogadta a meghívást '\$title' eseményre.\n\nIdőpont: \$date\nRésztvevők: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender feltételesen elfogadta a meghívást '\$title' eseményre.\n\nIdőpont: \$date\nRésztvevők: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender elutasította a meghívást '\$title' eseményre.\n\nIdőpont: \$date\nRésztvevők: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender elutasította a részvételét '\$title' eseményen, \$date időpontra";
+$labels['itipdeclineevent'] = 'Biztos benne, hogy el szeretné utasítani ezt a meghívást?';
+$labels['declinedeleteconfirm'] = 'Ki is szeretné törölni a saját naptárából ezt az időpontot?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['notanattendee'] = 'Ön nem szerepel az esemény meghívottai között';
+$labels['eventcancelled'] = 'Az esemény le lett mondva';
+$labels['saveincalendar'] = 'Mentés naptárba';
+$labels['updatemycopy'] = 'Naptárbejegyzés frissítése';
+$labels['resource'] = 'Eszközök';
+$labels['addresource'] = 'Eszköz foglalása';
+$labels['findresources'] = 'Eszközök keresése';
+$labels['resourcedetails'] = 'Részletek';
+$labels['resourceavailability'] = 'Elérhetőség';
+$labels['resourceowner'] = 'Tulajdonos';
+$labels['resourceadded'] = 'Az eszköz le lett foglalva az eseményhez';
+$labels['tabsummary'] = 'Részletek';
+$labels['tabrecurrence'] = 'Ismétlődés';
+$labels['tabattendees'] = 'Résztvevők';
+$labels['tabresources'] = 'Eszközök';
+$labels['tabattachments'] = 'Csatolmányok';
+$labels['tabsharing'] = 'Megosztás';
+$labels['deleteobjectconfirm'] = 'Biztos benne, hogy törölni szeretné ezt az eseményt?';
+$labels['deleteventconfirm'] = 'Biztos benne, hogy törölni szeretné ezt az eseményt?';
+$labels['deletecalendarconfirm'] = 'Biztos benne, hogy törölni szeretné ezt a naptárat, az összes benne lévő eseménnyel?';
+$labels['deletecalendarconfirmrecursive'] = 'Biztos benne, hogy törölni szeretné ezt a naptárat, az összes benne lévő al-naptárral és eseménnyel?';
+$labels['savingdata'] = 'Adatok mentése...';
+$labels['errorsaving'] = 'A módosításokat nem sikerült elmenteni.';
+$labels['operationfailed'] = 'A műveletet nem sikerült elvégezni.';
+$labels['invalideventdates'] = 'Helytelen dátumokoat adott meg! Kérem, ellenörizze a bevírt adatokat.';
+$labels['invalidcalendarproperties'] = 'Helytelen naptár tulajdonságok! Kérem, adjon meg egy helyes nevet.';
+$labels['searchnoresults'] = 'Nem található esemény a kiválasztott naptárakban.';
+$labels['successremoval'] = 'Az esemény törlése sikerült.';
+$labels['successrestore'] = 'Az esemény sikeresen vissza lett állítva.';
+$labels['errornotifying'] = 'Nem sikerült meghívókat küldeni a résztvevőknek.';
+$labels['errorimportingevent'] = 'Az esemény importálása sikertelen volt.';
+$labels['newerversionexists'] = 'Egy újabb verzió már van ebből az eseményből! Itt abbahagytam.';
+$labels['nowritecalendarfound'] = 'No calendar found to save the event';
+$labels['importedsuccessfully'] = 'Az esemény a \'$calendar\' naptárhoz hozzá lett adva';
+$labels['attendeupdateesuccess'] = 'A résztvevők adatai sikeresen frissítve';
+$labels['itipsendsuccess'] = 'Értesítés küldve a résztvevőknek';
+$labels['itipresponseerror'] = 'Nem sikerült választ küldeni erre a meghívásra';
+$labels['itipinvalidrequest'] = 'Ez a meghívás már érvénytelen';
+$labels['sentresponseto'] = 'Értesítésre válasz küldve: $mailto';
+$labels['localchangeswarning'] = 'Ezek a változások csak az ön naptárában fognak megjelenni, az esemény szervezőjénél nem.';
+$labels['importsuccess'] = '$nr esemény sikeresen importálva lett';
+$labels['importnone'] = 'Nem található importálható esemény.';
+$labels['importerror'] = 'Hiba importálás közben';
+$labels['aclnorights'] = 'Nincs adminisztrátori joga ehhez a naptárhoz';
+$labels['changeeventconfirm'] = 'Bejegyzés módosítása';
+$labels['changerecurringeventwarning'] = 'Ez egy ismétlődő esemény. Csak ezt az előfordulást szeretné módosítani, esetleg ezt az előfordulást az összes következővel, netán a teljes sorozatot, vagy új eseményként legyen mentve?';
+$labels['currentevent'] = 'Csak ezt';
+$labels['futurevents'] = 'Ezt és a következőeket';
+$labels['allevents'] = 'Egész sorozatot';
+$labels['saveasnew'] = 'Mentés újként';
+$labels['birthdays'] = 'Születésnapok';
+$labels['birthdayscalendar'] = 'Születésnapi naptár';
+$labels['displaybirthdayscalendar'] = 'Születésnapok megjelenítése';
+$labels['birthdayscalendarsources'] = 'Alábbi címjegyzékekből:';
+$labels['birthdayeventtitle'] = '$name születésnapja';
+$labels['birthdayage'] = '$age éves';
+?>
diff --git a/calendar/localization/it_IT.inc b/calendar/localization/it_IT.inc
new file mode 100644
index 0000000..2cc3ce7
--- /dev/null
+++ b/calendar/localization/it_IT.inc
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Visualizzazione predefinita';
+$labels['time_format'] = 'Formato ora';
+$labels['timeslots'] = 'Timeslots per ora';
+$labels['first_day'] = 'Inizio settimana';
+$labels['first_hour'] = 'Prima ora da mostrare';
+$labels['workinghours'] = 'Orario lavorativo';
+$labels['add_category'] = 'Aggiungi categoria';
+$labels['remove_category'] = 'Rimuovi categoria';
+$labels['defaultcalendar'] = 'Crea nuovi eventi in';
+$labels['eventcoloring'] = 'Colorazione evento';
+$labels['coloringmode0'] = 'Secondo il calendario';
+$labels['coloringmode1'] = 'Secondo la categoria';
+$labels['coloringmode2'] = 'Calendar for outline, category for content';
+$labels['coloringmode3'] = 'Category for outline, calendar for content';
+$labels['afternothing'] = 'Nessuna azione';
+$labels['aftertrash'] = 'Sposta nel cestino';
+$labels['afterdelete'] = 'Cancella il messaggio';
+$labels['afterflagdeleted'] = 'Segna come cancellato';
+$labels['aftermoveto'] = 'Sposta in...';
+$labels['itipoptions'] = 'Inviti all\'evento';
+$labels['afteraction'] = 'Dopo un invito o un aggiornamento il messaggio è processato';
+$labels['calendar'] = 'Calendario';
+$labels['calendars'] = 'Calendari';
+$labels['category'] = 'Categoria';
+$labels['categories'] = 'Categorie';
+$labels['createcalendar'] = 'Crea nuovo calendario';
+$labels['editcalendar'] = 'Modifica proprietà calendario';
+$labels['name'] = 'Nome';
+$labels['color'] = 'Colore';
+$labels['day'] = 'Giorno';
+$labels['week'] = 'Settimana';
+$labels['month'] = 'Mese';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nuovo';
+$labels['new_event'] = 'Nuovo evento';
+$labels['edit_event'] = 'Modifica evento';
+$labels['edit'] = 'Modifica';
+$labels['save'] = 'Salva';
+$labels['removelist'] = 'Rimuovi dalla lista';
+$labels['cancel'] = 'Annulla';
+$labels['select'] = 'Seleziona';
+$labels['print'] = 'Stampa';
+$labels['printtitle'] = 'Stampa calendari';
+$labels['title'] = 'Oggetto';
+$labels['description'] = 'Descrizione';
+$labels['all-day'] = 'Tutto il giorno';
+$labels['export'] = 'Esporta';
+$labels['exporttitle'] = 'Esporta come iCalendar';
+$labels['exportrange'] = 'Eventi di';
+$labels['exportattachments'] = 'Con allegati';
+$labels['customdate'] = 'Data personalizzata';
+$labels['location'] = 'Luogo';
+$labels['url'] = 'URL';
+$labels['date'] = 'Data';
+$labels['start'] = 'Inizio';
+$labels['starttime'] = 'Ora di inizio';
+$labels['end'] = 'Fine';
+$labels['endtime'] = 'Ora di fine';
+$labels['repeat'] = 'Ricorrenza';
+$labels['selectdate'] = 'Scegliere la data';
+$labels['freebusy'] = 'Mostrami come';
+$labels['free'] = 'Libero';
+$labels['busy'] = 'Occupato';
+$labels['outofoffice'] = 'Fuori Ufficio';
+$labels['tentative'] = 'Provvisorio';
+$labels['mystatus'] = 'Il mio stato';
+$labels['status'] = 'Stato';
+$labels['status-confirmed'] = 'Confermato';
+$labels['status-cancelled'] = 'Cancellato';
+$labels['priority'] = 'Priorità';
+$labels['sensitivity'] = 'Privacy';
+$labels['public'] = 'pubblico';
+$labels['private'] = 'privato';
+$labels['confidential'] = 'confidenziale';
+$labels['links'] = 'Riferimento';
+$labels['alarms'] = 'Promemoria';
+$labels['comment'] = 'Commento';
+$labels['created'] = 'Creato';
+$labels['changed'] = 'Ultima modifica';
+$labels['unknown'] = 'Sconosciuto';
+$labels['eventoptions'] = 'Opzioni';
+$labels['generated'] = 'generato il';
+$labels['eventhistory'] = 'Storico';
+$labels['removelink'] = 'Rimuovi riferimento email';
+$labels['printdescriptions'] = 'Stampa descrizioni';
+$labels['parentcalendar'] = 'Inserisci dentro';
+$labels['searchearlierdates'] = '« Cerca eventi precedenti';
+$labels['searchlaterdates'] = 'Cerca eventi successivi »';
+$labels['andnmore'] = 'Altri $nr...';
+$labels['togglerole'] = 'Fare clic per cambiare il ruolo';
+$labels['createfrommail'] = 'Salva come evento';
+$labels['importevents'] = 'Importa eventi';
+$labels['importrange'] = 'Eventi di';
+$labels['onemonthback'] = '1 mese prima';
+$labels['nmonthsback'] = '$nr mesi prima';
+$labels['showurl'] = 'Mostra URL calendario';
+$labels['showurldescription'] = 'Usare il seguente indirizzo per accedere (in sola lettura) al calendario da altre applicazioni. È possibile copiarlo e incollarlo in qualsiasi software che supporta il formato iCal.';
+$labels['caldavurldescription'] = 'Copiare questo indirizzo in un\'applicazione client <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> (es. Evolution o Mozilla Thunderbird) per sincronizzare completamente questo specifico calendario con il proprio computer o dispositivo mobile.';
+$labels['findcalendars'] = 'Trova calendari...';
+$labels['searchterms'] = 'Cerca elemento';
+$labels['calsearchresults'] = 'Calendari disponibili';
+$labels['calendarsubscribe'] = 'Elenca sempre';
+$labels['nocalendarsfound'] = 'Nessun calendario trovato';
+$labels['nrcalendarsfound'] = '$nr calendari trovati';
+$labels['quickview'] = 'Visualizza solo questo calendario';
+$labels['invitationspending'] = 'Inviti in sospeso';
+$labels['invitationsdeclined'] = 'Inviti scartati';
+$labels['changepartstat'] = 'Cambia lo stato del partecipante';
+$labels['rsvpcomment'] = 'Testo dell\'invito';
+$labels['listrange'] = 'Intervallo da visualizzare:';
+$labels['listsections'] = 'Dividi in:';
+$labels['smartsections'] = 'Sezioni intelligenti';
+$labels['until'] = 'fino a';
+$labels['today'] = 'Oggi';
+$labels['tomorrow'] = 'Domani';
+$labels['thisweek'] = 'Questa settimana';
+$labels['nextweek'] = 'Prossima settimana';
+$labels['prevweek'] = 'Settimana precedente';
+$labels['thismonth'] = 'Questo mese';
+$labels['nextmonth'] = 'Prossimo mese';
+$labels['weekofyear'] = 'Settimana';
+$labels['pastevents'] = 'Passato';
+$labels['futureevents'] = 'Futuro';
+$labels['showalarms'] = 'Mostra promemoria';
+$labels['defaultalarmtype'] = 'Impostazioni predefinite dei promemoria';
+$labels['defaultalarmoffset'] = 'Tempo predefinito per i promemoria';
+$labels['attendee'] = 'Partecipante';
+$labels['role'] = 'Ruolo';
+$labels['availability'] = 'Dispon.';
+$labels['confirmstate'] = 'Stato';
+$labels['addattendee'] = 'Aggiungi partecipante';
+$labels['roleorganizer'] = 'Organizzatore';
+$labels['rolerequired'] = 'Necessario';
+$labels['roleoptional'] = 'Facoltativo';
+$labels['rolechair'] = 'Presidente';
+$labels['rolenonparticipant'] = 'Assente';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Gruppo';
+$labels['cutyperesource'] = 'Risorsa';
+$labels['cutyperoom'] = 'Stanza';
+$labels['availfree'] = 'Libero';
+$labels['availbusy'] = 'Occupato';
+$labels['availunknown'] = 'Sconosciuto';
+$labels['availtentative'] = 'Provvisorio';
+$labels['availoutofoffice'] = 'Fuori sede';
+$labels['delegatedto'] = 'Delegato a:';
+$labels['delegatedfrom'] = 'Delegato da:';
+$labels['scheduletime'] = 'Trova disponibilità';
+$labels['sendinvitations'] = 'Manda inviti';
+$labels['sendnotifications'] = 'Notifica le modifiche ai partecipanti';
+$labels['sendcancellation'] = 'Notifica ai partecipanti la cancellazione dell\'evento';
+$labels['onlyworkinghours'] = 'Trova disponibilità durante le ore lavorative';
+$labels['reqallattendees'] = 'Necessario/tutti i partecipanti';
+$labels['prevslot'] = 'Spazio precedente';
+$labels['nextslot'] = 'Spazio successivo';
+$labels['suggestedslot'] = 'Spazio suggerito';
+$labels['noslotfound'] = 'Impossibile trovare uno spazio di tempo libero';
+$labels['invitationsubject'] = 'Sei stato invitato a "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nQuando: \$date\n\nInvitati: \$attendees\n\nIn allegato un file iCalendar con tutti i dettagli dell'evento, che puoi importare nella tua applicazione calendario.";
+$labels['invitationattendlinks'] = "Se il tuo client di posta elettronica non supporta le richieste iTip, puoi seguire il seguente collegamento per accettare o rifiutare l'invito:\n\$url";
+$labels['eventupdatesubject'] = '"$title" è stato aggiornato';
+$labels['eventupdatesubjectempty'] = 'Un evento che ti riguarda è stato aggiornato';
+$labels['eventupdatemailbody'] = "*\$title*\n\nQuando: \$date\n\nInvitati: \$attendees\n\nIn allegato un file iCalendar con i dettagli aggiornati dell'evento che puoi importare nella tua applicazione calendario.";
+$labels['eventcancelsubject'] = '"$title" è stato annullato';
+$labels['eventcancelmailbody'] = "*\$title*\n\nQuando: \$date\n\nInvitati: \$attendees\n\nL'evento è stato cancellato da \$organizer.\n\nIn allegato un file iCalendar con i dettagli aggiornati dell'evento .";
+$labels['itipobjectnotfound'] = 'L\'evento al quale questo messaggio fa riferimento non è stato trovato nel tuo calendario.';
+$labels['itipmailbodyaccepted'] = "\$sender ha accettato l'invito al seguente evento:\n\n*\$title*\n\nQuando: \$date\n\nInvitati: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender ha accettato con riserva l'invito al seguente evento:\n\n*\$title*\n\nQuando: \$date\n\nInvitati: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender ha rifiutato l'invito al seguente evento:\n\n*\$title*\n\nQuando: \$date\n\nInvitati: \$attendees";
+$labels['itipmailbodycancel'] = "\$Il mittente ha rifiutato la tua partecipazione nel seguente evento:\n\n*\$title*\n\nQuando: \$date";
+$labels['itipmailbodydelegated'] = "\$Il mittente ha delegato la partecipazione nel seguente evento:\n\n*\$title*\n\nQuando: \$date";
+$labels['itipmailbodydelegatedto'] = "\$Il mittente ha delegato a te la partecipazione nel seguente evento:\n\n*\$title*\n\nQuando:\$date";
+$labels['itipdeclineevent'] = 'Vuoi rifiutare l\'invito a questo evento?';
+$labels['declinedeleteconfirm'] = 'Vuoi anche cancellare dal calendario l\'evento rifiutato?';
+$labels['itipcomment'] = 'Commento all\'invito/notifica';
+$labels['itipcommenttitle'] = 'Questo commento verrà allegato al messaggio di invito/notifica spedito ai partecipanti';
+$labels['notanattendee'] = 'Non sei elencato tra i partecipanti a questo evento';
+$labels['eventcancelled'] = 'L\'evento è stato annullato';
+$labels['saveincalendar'] = 'salva in';
+$labels['updatemycopy'] = 'Aggiorna nel mio calendario';
+$labels['savetocalendar'] = 'Salva sul calendario';
+$labels['openpreview'] = 'Controlla calendario';
+$labels['noearlierevents'] = 'Non ci sono eventi precedenti';
+$labels['nolaterevents'] = 'Non ci sono eventi successivi';
+$labels['resource'] = 'Risorsa';
+$labels['addresource'] = 'Prenota risorsa';
+$labels['findresources'] = 'Trova risorse';
+$labels['resourcedetails'] = 'Dettagli';
+$labels['resourceavailability'] = 'Disponibilità';
+$labels['resourceowner'] = 'Proprietario';
+$labels['resourceadded'] = 'La risorsa è stata aggiunta al tuo evento';
+$labels['tabsummary'] = 'Riepilogo';
+$labels['tabrecurrence'] = 'Ricorrenza';
+$labels['tabattendees'] = 'Partecipanti';
+$labels['tabresources'] = 'Risorse';
+$labels['tabattachments'] = 'Allegati';
+$labels['tabsharing'] = 'Condivisione';
+$labels['deleteobjectconfirm'] = 'Cancellare davvero questo evento?';
+$labels['deleteventconfirm'] = 'Cancellare davvero questo evento?';
+$labels['deletecalendarconfirm'] = 'Cancellare davvero questo calendario con tutti i suoi eventi?';
+$labels['deletecalendarconfirmrecursive'] = 'Vuoi veramente eliminare questo calendario con tutti i suoi eventi e i suoi sotto-calendari?';
+$labels['savingdata'] = 'Salvataggio dati...';
+$labels['errorsaving'] = 'Impossibile salvare le modifiche.';
+$labels['operationfailed'] = 'L\'operazione richiesta è fallita.';
+$labels['invalideventdates'] = 'Le date inserite non sono valide. Controllare l\'inserimento.';
+$labels['invalidcalendarproperties'] = 'Proprietà del calendario non valide. Impostare un nome valido.';
+$labels['searchnoresults'] = 'Nessun evento trovato nel calendario selezionato.';
+$labels['successremoval'] = 'L\'evento è stato cancellato correttamente.';
+$labels['successrestore'] = 'L\'evento è stato ripristinato correttamente.';
+$labels['errornotifying'] = 'Spedizione delle notifiche ai partecipanti dell\'evento fallita';
+$labels['errorimportingevent'] = 'Importazione evento fallita';
+$labels['importwarningexists'] = 'Una copia di questo evento esiste già nel tuo calendario';
+$labels['newerversionexists'] = 'Esiste già una versione più recente di questo evento. Abortito.';
+$labels['nowritecalendarfound'] = 'Non c\'è nessun calendario dove salvare l\'evento';
+$labels['importedsuccessfully'] = 'Evento aggiunto correttamente a \'$calendar\'';
+$labels['updatedsuccessfully'] = 'L\'evento è stato aggiornato con successo su \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Stato dei partecipanti aggiornato correttamente';
+$labels['itipsendsuccess'] = 'Invito spedito ai partecipanti.';
+$labels['itipresponseerror'] = 'Spedizione della risposta all\'invito fallita';
+$labels['itipinvalidrequest'] = 'Questo invito non è più valido';
+$labels['sentresponseto'] = 'Risposta all\'invito inviata correttamente a $mailto';
+$labels['localchangeswarning'] = 'Stai per fare dei cambiamenti che compariranno solo nel tuo calendario e non saranno spediti all\'organizzatore dell\'evento.';
+$labels['importsuccess'] = '$nr eventi importati correttamente';
+$labels['importnone'] = 'Nessun evento trovato da importare';
+$labels['importerror'] = 'Si è verificato un errore durante l\'importazione';
+$labels['aclnorights'] = 'Non hai i diritti di amministratore per questo calendario.';
+$labels['changeeventconfirm'] = 'Cambia evento';
+$labels['removeeventconfirm'] = 'Cancella evento';
+$labels['changerecurringeventwarning'] = 'This is a recurring event. Would you like to edit the current event only, this and all future occurences, all occurences or save it as a new event?';
+$labels['removerecurringeventwarning'] = 'Questo è un evento ricorrente. Vuoi cancellare solamente l\'evento corrente, quest\'ultimo e tutte le future ricorrenze oppure tutte le ricorrenze di questo evento?';
+$labels['currentevent'] = 'Current';
+$labels['futurevents'] = 'Futuro';
+$labels['allevents'] = 'Tutto';
+$labels['saveasnew'] = 'Salva come nuovo';
+$labels['birthdays'] = 'Compleanni';
+$labels['birthdayscalendar'] = 'Calendario compleanni';
+$labels['displaybirthdayscalendar'] = 'Mostra il calendario compleanni';
+$labels['birthdayscalendarsources'] = 'Da queste rubriche';
+$labels['birthdayeventtitle'] = 'Compleanno di $name';
+$labels['birthdayage'] = 'Età: $age anni';
+$labels['objectchangelog'] = 'Storico modifiche';
+$labels['revision'] = 'Revisione';
+$labels['user'] = 'Utente';
+$labels['operation'] = 'Azione';
+$labels['actionappend'] = 'Salvato';
+$labels['actionmove'] = 'Spostato';
+$labels['actiondelete'] = 'Cancellato';
+$labels['compare'] = 'Confronta';
+$labels['showrevision'] = 'Mostra questa versione';
+$labels['restore'] = 'Rirpistina questa versione';
+$labels['objectnotfound'] = 'Caricamento dati dell\'evento fallito';
+$labels['objectchangelognotavailable'] = 'Lo storico modifiche non è disponibile per questo evento';
+$labels['objectdiffnotavailable'] = 'Nessun confronto possibile tra le revisioni selezionate';
+$labels['revisionrestoreconfirm'] = 'Vuoi veramente ripristinare la revisione $rev di questo evento? L\'evento corrente verrà sostituito dalla vecchia versione.';
+$labels['arialabelminical'] = 'Selezione della data del calendario';
+$labels['arialabelcalendarview'] = 'Vista calendario';
+$labels['arialabelsearchform'] = 'Modulo ricerca evento';
+$labels['arialabelquicksearchbox'] = 'Inserimento ricerca evento';
+$labels['arialabelcalsearchform'] = 'Modulo ricerca calendari';
+$labels['calendaractions'] = 'Azione calendari';
+$labels['arialabeleventattendees'] = 'Lista partecipanti all\'evento';
+$labels['arialabeleventresources'] = 'Lista risorse dell\'evento';
+$labels['arialabelresourcesearchform'] = 'Modulo ricerca risorse';
+$labels['arialabelresourceselection'] = 'Risorse disponibili';
+?>
diff --git a/calendar/localization/ja_JP.inc b/calendar/localization/ja_JP.inc
new file mode 100644
index 0000000..06b5c89
--- /dev/null
+++ b/calendar/localization/ja_JP.inc
@@ -0,0 +1,177 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'デフォルトビュー';
+$labels['time_format'] = '時刻表示形式';
+$labels['first_day'] = '最初の平日';
+$labels['first_hour'] = '最初の時間を表示';
+$labels['workinghours'] = '労働時間';
+$labels['add_category'] = 'カテゴリ追加';
+$labels['remove_category'] = 'カテゴリ削除';
+$labels['defaultcalendar'] = '新しいイベントの作成';
+$labels['eventcoloring'] = 'イベントカラー';
+$labels['coloringmode0'] = 'カレンダーの説明';
+$labels['coloringmode1'] = 'カテゴリの説明';
+$labels['coloringmode2'] = 'アウトライン用カレンダー、コンテンツ用カテゴリ';
+$labels['coloringmode3'] = 'アウトライン用カレンダー、コンテンツ用カテゴリ';
+$labels['calendar'] = 'カレンダー';
+$labels['calendars'] = 'カレンダー';
+$labels['category'] = 'カテゴリ';
+$labels['categories'] = 'カテゴリ';
+$labels['createcalendar'] = '新しいカレンダーの作成';
+$labels['editcalendar'] = 'カレンダーのプロパティ編集';
+$labels['name'] = '名前';
+$labels['color'] = '色';
+$labels['day'] = '日';
+$labels['week'] = '週';
+$labels['month'] = '月';
+$labels['agenda'] = '予定表';
+$labels['new'] = '新規';
+$labels['new_event'] = '新規イベント';
+$labels['edit_event'] = 'イベント編集';
+$labels['edit'] = '編集';
+$labels['save'] = '保存';
+$labels['cancel'] = 'キャンセル';
+$labels['select'] = '選択';
+$labels['print'] = '印刷';
+$labels['printtitle'] = 'カレンダー印刷';
+$labels['title'] = '要約';
+$labels['description'] = '説明';
+$labels['all-day'] = '全日';
+$labels['export'] = 'エクスポート';
+$labels['exporttitle'] = 'iカレンダーへエクスポート';
+$labels['exportrange'] = 'イベント元';
+$labels['location'] = '場所';
+$labels['date'] = '期日';
+$labels['start'] = '開始';
+$labels['end'] = '終了';
+$labels['repeat'] = '繰返し';
+$labels['selectdate'] = '日付選択';
+$labels['freebusy'] = '表示する';
+$labels['free'] = '空';
+$labels['busy'] = 'ビジー';
+$labels['outofoffice'] = '外出';
+$labels['tentative'] = '仮';
+$labels['status'] = '状態';
+$labels['priority'] = '優先度';
+$labels['sensitivity'] = 'プライバシー';
+$labels['public'] = 'パブリック';
+$labels['private'] = 'プライベート';
+$labels['confidential'] = '親展';
+$labels['alarms'] = '通知';
+$labels['unknown'] = '不明';
+$labels['eventoptions'] = 'オプション';
+$labels['generated'] = '生成';
+$labels['printdescriptions'] = '説明印刷';
+$labels['parentcalendar'] = '内に挿入';
+$labels['searchearlierdates'] = '<< 以前のイベントを検索';
+$labels['searchlaterdates'] = '今後のイベントの検索 >>';
+$labels['andnmore'] = '$nr さらに…';
+$labels['togglerole'] = 'クリックでロールをトグル';
+$labels['createfrommail'] = 'イベントとして保存';
+$labels['importevents'] = 'イベントのインポート';
+$labels['importrange'] = 'イベント元';
+$labels['onemonthback'] = '1 ヶ月戻る';
+$labels['nmonthsback'] = '$nr ヶ月戻る';
+$labels['showurl'] = 'カレンダーURL表示';
+$labels['showurldescription'] = '以下のアドレスを使用して他のアプリケーションからカレンダーにアクセス(読込のみ)できます。iCal形式をサポートしたカレンダーソフトウェアへコピーアンドペーストができます。';
+$labels['listrange'] = '表示範囲:';
+$labels['listsections'] = '分割:';
+$labels['smartsections'] = 'スマートセクション';
+$labels['until'] = 'いつまでに';
+$labels['today'] = '今日';
+$labels['tomorrow'] = '明日';
+$labels['thisweek'] = '今週';
+$labels['nextweek'] = '来週';
+$labels['thismonth'] = '今月';
+$labels['nextmonth'] = '来月';
+$labels['weekofyear'] = '週';
+$labels['pastevents'] = '以前';
+$labels['futureevents'] = '以降';
+$labels['defaultalarmtype'] = 'デフォルト通知設定';
+$labels['defaultalarmoffset'] = 'デフォルト通知時間';
+$labels['attendee'] = '参加者';
+$labels['role'] = 'ロール';
+$labels['availability'] = '利用可';
+$labels['confirmstate'] = '状態';
+$labels['addattendee'] = '参加者追加';
+$labels['roleorganizer'] = '編成者';
+$labels['rolerequired'] = '要件';
+$labels['roleoptional'] = 'オプション';
+$labels['cutypegroup'] = 'グループ';
+$labels['cutyperesource'] = 'リソース';
+$labels['availfree'] = '空';
+$labels['availbusy'] = 'ビジー';
+$labels['availunknown'] = '不明';
+$labels['availtentative'] = '仮';
+$labels['availoutofoffice'] = '外出';
+$labels['scheduletime'] = '利用可検索';
+$labels['sendinvitations'] = '招待を送る';
+$labels['sendnotifications'] = '変更を参加者へ通知する';
+$labels['sendcancellation'] = '中止を参加者へ通知する';
+$labels['onlyworkinghours'] = '労働時間内の利用可検索';
+$labels['reqallattendees'] = '要件/全参加者';
+$labels['prevslot'] = '前のスロット';
+$labels['nextslot'] = '次のスロット';
+$labels['noslotfound'] = '空スロットを見つけられません';
+$labels['invitationsubject'] = '"$title" に招待されました';
+$labels['invitationmailbody'] = "*\$title*\n\nいつ: \$date\n\n招待者: \$attendees\n\nあなたのカレンダーアプリケーションにインポートできる全イベントの詳細がインポートできる添付されたiカレンダーファイルを見つけてください。";
+$labels['invitationattendlinks'] = "この場合あなたのメールクライアントはiTip リクエストをサポートしてません、以下のリンクからこの招待を承諾もしくは辞退してください:\n\$url";
+$labels['eventupdatesubject'] = '"$title" はアップデートされました';
+$labels['eventupdatesubjectempty'] = 'あなたに関連するイベントが更新されました';
+$labels['eventupdatemailbody'] = "*\$title*\n\nいつ: \$date\n\n招待者: \$attendees\n\nあなたのカレンダーアプリケーションにインポートできるアップデートされた全イベントの詳細が添付されたiカレンダーファイルを見つけてください。";
+$labels['eventcancelsubject'] = '"$title" は変更されました';
+$labels['eventcancelmailbody'] = "*\$title*\n\nいつ: \$date\n\n招待者: \$attendees\n\nイベントが \$organizer によってキャンセルされました。\n\n更新されたイベントの詳細とともに添付されたiカレンダーファイルを見つけてください。";
+$labels['itipmailbodyaccepted'] = "\$sender は以下のイベントへの招待を承諾しました:\n\n*\$title*\n\nいつ: \$date\n\n招待者: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender は以下のイベントへの招待を仮承諾しました:\n\n*\$title*\n\nいつ: \$date\n\n招待者: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender は以下のイベントへの招待を辞退しました:\n\n*\$title*\n\nいつ: \$date\n\n招待者: \$attendees";
+$labels['itipdeclineevent'] = 'このイベントへの招待を辞退しますか?';
+$labels['notanattendee'] = 'このイベントの出席者として一覧にありません';
+$labels['eventcancelled'] = 'このイベントはキャンセルされました';
+$labels['saveincalendar'] = '保存';
+$labels['resource'] = 'リソース';
+$labels['resourcedetails'] = '詳細';
+$labels['tabsummary'] = '要約';
+$labels['tabrecurrence'] = '繰返し';
+$labels['tabattendees'] = '参加者';
+$labels['tabresources'] = 'リソース';
+$labels['tabattachments'] = '添付';
+$labels['tabsharing'] = '共有';
+$labels['deleteobjectconfirm'] = '本当にこのイベントを削除しますか?';
+$labels['deleteventconfirm'] = '本当にこのイベントを削除しますか?';
+$labels['deletecalendarconfirm'] = '本当にこのカレンダーを全イベントとともに削除しますか?';
+$labels['savingdata'] = 'データを保存中…';
+$labels['errorsaving'] = '変更が保存できませんでした。';
+$labels['operationfailed'] = '要求された操作ができませんでした。';
+$labels['invalideventdates'] = '無効な日付が入力されました! 入力を確認してください。';
+$labels['invalidcalendarproperties'] = '無効なカレンダーのプロパティです! 有効な名前を設定してください。';
+$labels['searchnoresults'] = '選択されたカレンダーにイベントは見つかりません。';
+$labels['successremoval'] = 'イベントを削除しました';
+$labels['successrestore'] = 'イベントを復旧しました';
+$labels['errornotifying'] = 'イベント参加者への通知が送信できませんでした';
+$labels['errorimportingevent'] = 'イベントのインポートができませんでした';
+$labels['newerversionexists'] = 'このイベントのより新しいヴァージョンがすでにあります。中断されました。';
+$labels['nowritecalendarfound'] = 'イベントを保存するカレンダーが見つかりません';
+$labels['importedsuccessfully'] = '\'$calendar\' へイベントを追加しました';
+$labels['attendeupdateesuccess'] = '出席者状況を更新しました';
+$labels['itipsendsuccess'] = '出席者へ招待を送信しました。';
+$labels['itipresponseerror'] = 'この招待の返信できませんでした';
+$labels['itipinvalidrequest'] = 'この招待は間もなく無効になります';
+$labels['sentresponseto'] = '$mailto への招待の返信しました';
+$labels['importsuccess'] = '$nr イベントをインポートしました';
+$labels['importnone'] = 'インポートされたイベントはありません';
+$labels['importerror'] = 'インポート中にエラーが発生しました。';
+$labels['aclnorights'] = 'このカレンダーの管理権限がありません。';
+$labels['changeeventconfirm'] = 'イベント変更';
+$labels['changerecurringeventwarning'] = 'これは繰返しイベントです。現在のイベントのみ、このイベントと今後の全イベント、全イベント、編集したい、もしくは新しいイベントとして保存したい?';
+$labels['currentevent'] = '現在';
+$labels['futurevents'] = '今後';
+$labels['allevents'] = '全て';
+$labels['saveasnew'] = '新規保存';
+$labels['user'] = 'ユーザ';
+?>
diff --git a/calendar/localization/nl_NL.inc b/calendar/localization/nl_NL.inc
new file mode 100644
index 0000000..6b55a14
--- /dev/null
+++ b/calendar/localization/nl_NL.inc
@@ -0,0 +1,229 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Standaard agenda';
+$labels['time_format'] = 'Tijdsformaat';
+$labels['timeslots'] = 'Tijdsdelen per uur';
+$labels['first_day'] = 'Eerste weekdag';
+$labels['first_hour'] = 'Eerste uur om weer te geven';
+$labels['workinghours'] = 'Werkuren';
+$labels['add_category'] = 'Categorie toevoegen';
+$labels['remove_category'] = 'Categorie verwijderen';
+$labels['defaultcalendar'] = 'Maak nieuwe afspraken in';
+$labels['eventcoloring'] = 'Kleuren van afspraken';
+$labels['coloringmode0'] = 'Volgens kalender';
+$labels['coloringmode1'] = 'Volgens categorie';
+$labels['coloringmode2'] = 'Kalender voor omtrek, categorie voor inhoud';
+$labels['coloringmode3'] = 'Categorie voor omtrek, kalender voor inhoud';
+$labels['aftertrash'] = 'Verplaats naar prullenbak';
+$labels['afterdelete'] = 'Verwijder bericht';
+$labels['afterflagdeleted'] = 'Markeer als verwijderd';
+$labels['aftermoveto'] = 'Verplaats naar...';
+$labels['calendar'] = 'Kalender';
+$labels['calendars'] = 'Kalenders';
+$labels['category'] = 'Categorie';
+$labels['categories'] = 'Categorieën';
+$labels['createcalendar'] = 'Maak een nieuwe kalender';
+$labels['editcalendar'] = 'Wijzig kalender eigenschappen';
+$labels['name'] = 'Naam';
+$labels['color'] = 'Kleur';
+$labels['day'] = 'Dag';
+$labels['week'] = 'Week';
+$labels['month'] = 'Maand';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nieuw';
+$labels['new_event'] = 'Nieuwe afspraak';
+$labels['edit_event'] = 'Wijzig afspraak';
+$labels['edit'] = 'Wijzig';
+$labels['save'] = 'Bewaar';
+$labels['cancel'] = 'Terug';
+$labels['select'] = 'Selecteer';
+$labels['print'] = 'Afdrukken';
+$labels['printtitle'] = 'Kalenders afdrukken';
+$labels['title'] = 'Samenvatting';
+$labels['description'] = 'Omschrijving';
+$labels['all-day'] = 'hele dag';
+$labels['export'] = 'Exporteer naar ICS';
+$labels['exporttitle'] = 'Als iCalender exporteren';
+$labels['exportrange'] = 'Afspraken uit';
+$labels['exportattachments'] = 'Exporteer bijlages';
+$labels['customdate'] = 'Aangepaste datumweergave';
+$labels['location'] = 'Locatie';
+$labels['url'] = 'URL';
+$labels['date'] = 'Datum';
+$labels['start'] = 'Begin';
+$labels['starttime'] = 'Start time';
+$labels['end'] = 'Eind';
+$labels['endtime'] = 'Eindtijd';
+$labels['repeat'] = 'Herhaal';
+$labels['selectdate'] = 'Kies datum';
+$labels['freebusy'] = 'Toon mij als';
+$labels['free'] = 'Vrij';
+$labels['busy'] = 'Bezet';
+$labels['outofoffice'] = 'Niet Aanwezig';
+$labels['tentative'] = 'Misschien';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Bevestigd';
+$labels['status-cancelled'] = 'Afgelast';
+$labels['priority'] = 'Prioriteit';
+$labels['sensitivity'] = 'Zichtbaarheid';
+$labels['public'] = 'publiek';
+$labels['private'] = 'prive';
+$labels['confidential'] = 'vertrouwelijk';
+$labels['alarms'] = 'Herinnering';
+$labels['comment'] = 'Opmerking';
+$labels['unknown'] = 'Onbekend';
+$labels['eventoptions'] = 'Opties';
+$labels['generated'] = 'gegenereerd op';
+$labels['eventhistory'] = 'Geschiedenis';
+$labels['printdescriptions'] = 'Print beschrijvingen';
+$labels['parentcalendar'] = 'Voeg toe in';
+$labels['searchearlierdates'] = '« Zoek voor eerdere afspraken';
+$labels['searchlaterdates'] = 'Zoek voor latere afspraken »';
+$labels['andnmore'] = '$nr meer...';
+$labels['togglerole'] = 'Klik om van rol te wisselen';
+$labels['createfrommail'] = 'Bewaar als afspraak';
+$labels['importevents'] = 'Afspraken importeren';
+$labels['importrange'] = 'Afspraken vanaf';
+$labels['onemonthback'] = '1 maand terug';
+$labels['nmonthsback'] = '$nr maanden terug';
+$labels['showurl'] = 'Toon kalender URL';
+$labels['showurldescription'] = 'Gebruik het volgende adres om uw kalendar te gebruiken (alleen lezen) in andere programma\'s. U kunt dit knippen en plakken in elk kalender programma dat het iCal formaat ondersteunt.';
+$labels['caldavurldescription'] = 'Kopieer dit adres in een <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> programma (bijv. Evolution of Mozilla Thunderbird) om deze specifieke kalender volledig te synchronizeren met je computer of mobiel apparaat.';
+$labels['findcalendars'] = 'Vind agenda\'s...';
+$labels['searchterms'] = 'Search terms';
+$labels['calsearchresults'] = 'Beschikbare agenda\'s';
+$labels['calendarsubscribe'] = 'Permanent weergeven';
+$labels['invitationsdeclined'] = 'Afgewezen uitnodigingen';
+$labels['listrange'] = 'Bereik om te tonen:';
+$labels['listsections'] = 'Verdeel in:';
+$labels['smartsections'] = 'Slimme secties';
+$labels['until'] = 'tot';
+$labels['today'] = 'Vandaag';
+$labels['tomorrow'] = 'Morgen';
+$labels['thisweek'] = 'Deze week';
+$labels['nextweek'] = 'Volgende week';
+$labels['thismonth'] = 'Deze maand';
+$labels['nextmonth'] = 'Volgende maand';
+$labels['weekofyear'] = 'Week';
+$labels['pastevents'] = 'Verleden';
+$labels['futureevents'] = 'Toekomst';
+$labels['showalarms'] = 'Toon herinneringen';
+$labels['defaultalarmtype'] = 'Instelling standaard herinnering';
+$labels['defaultalarmoffset'] = 'Standaard herinneringstijd';
+$labels['attendee'] = 'Deelnemer';
+$labels['role'] = 'Rol';
+$labels['availability'] = 'Beschikb.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Deelnemer toevoegen';
+$labels['roleorganizer'] = 'Organisatie';
+$labels['rolerequired'] = 'Verplicht';
+$labels['roleoptional'] = 'Optioneel';
+$labels['rolechair'] = 'Voorzitter';
+$labels['rolenonparticipant'] = 'Afwezig';
+$labels['cutypeindividual'] = 'Individueel';
+$labels['cutypegroup'] = 'Groep';
+$labels['cutyperesource'] = 'Middel';
+$labels['cutyperoom'] = 'Kamer';
+$labels['availfree'] = 'Vrij';
+$labels['availbusy'] = 'Bezet';
+$labels['availunknown'] = 'Onbekend';
+$labels['availtentative'] = 'Misschien';
+$labels['availoutofoffice'] = 'Niet Aanwezig';
+$labels['delegatedto'] = 'Gedelegeerd aan';
+$labels['delegatedfrom'] = 'Gedelegeerd door';
+$labels['scheduletime'] = 'Vind beschikbaarheid';
+$labels['sendinvitations'] = 'Verzend uitnodigingen';
+$labels['sendnotifications'] = 'Verzend notificaties';
+$labels['sendcancellation'] = 'Verzend annulering';
+$labels['onlyworkinghours'] = 'Vind beschikbaarheid binnen mijn werkuren';
+$labels['reqallattendees'] = 'Verplicht/alle deelnemers';
+$labels['prevslot'] = 'vorig voorstel';
+$labels['nextslot'] = 'volgend voorstel';
+$labels['noslotfound'] = 'Geen voorstel gevonden';
+$labels['invitationsubject'] = 'U bent uitgenodigd voor "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nWanneer: \$date\n\nGenodigden: \$attendees\n\nBijgevoegd vindt u een iCalendar bestand met alle details omtrent de afspraak die u kunt importeren in uw kalender programma.";
+$labels['invitationattendlinks'] = "In het geval dat uw email programma geen iTip verzoeken aankan, kunt u de volgende link gebruiken om deze uitnodiging te accepteren or af te wijzen :\n\$url";
+$labels['eventupdatesubject'] = '"$title" is gewijzigd';
+$labels['eventupdatesubjectempty'] = 'Een afspraak is gewijzigd';
+$labels['eventupdatemailbody'] = "*\$title*\n\nWanneer: \$date\n\nGenodigden: \$attendees\n\nBijgevoegd vindt u een iCalendar bestand met de gewijzigde details omtrent de afspraak die u kunt importeren in uw kalender programma.";
+$labels['eventcancelsubject'] = '"$title" is geannuleerd';
+$labels['eventcancelmailbody'] = "*\$title*\n\nWanneer: \$date\n\nGenodigden: \$attendees\n\nDeze afspraak is geannuleerd door \$organizer.\n\nBijgevoegd vindt u een iCalendar bestand met de gewijzigde details omtrent de afspraak";
+$labels['itipobjectnotfound'] = 'De afspraak waaraan door dit bericht wordt gereferreerd is niet gevonden in uw kalender.';
+$labels['itipmailbodyaccepted'] = "\$sender heeft de uitnodiging geaccepteerd voor de volgende afspraak:\n\n*\$title*\n\nWanneer: \$date\n\nGenodigden: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender heeft onder voorbehoud de uitnodiging geaccepteerd voor de volgende afspraak:\n\n*\$title*\n\nWanneer: \$date\n\nGenodigden: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender heeft de uitnodiging afgewezen voor de volgende afspraak:\n\n*\$title*\n\nWanneer: \$date\n\nGenodigden: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender heeft je deelname afgewezen voor de volgende afspraak:\n\n*\$title*\n\nWanneer: \$date";
+$labels['itipdeclineevent'] = 'Wil u de uitnodiging voor deze afspraak afwijzen?';
+$labels['declinedeleteconfirm'] = 'Wilt u tevens deze afgewezen uitnodiging verwijderen uit je kalender?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['notanattendee'] = 'U staat niet op de lijst van genodigden voor deze afspraak';
+$labels['eventcancelled'] = 'Dit evenement is afgelast';
+$labels['saveincalendar'] = 'bewaar in';
+$labels['updatemycopy'] = 'Wijzig in mijn kalender';
+$labels['savetocalendar'] = 'Opslaan in kalender';
+$labels['resource'] = 'Hulpmiddel';
+$labels['addresource'] = 'Voeg hulpmiddel toe';
+$labels['findresources'] = 'Vind hulpmiddelen';
+$labels['resourcedetails'] = 'Details';
+$labels['resourceavailability'] = 'Beschikbaarheid';
+$labels['resourceowner'] = 'Eigenaar';
+$labels['resourceadded'] = 'Hulpmiddel is toegevoegd aan uw afspraak';
+$labels['tabsummary'] = 'Samenvatting';
+$labels['tabrecurrence'] = 'Herhaling';
+$labels['tabattendees'] = 'Deelnemers';
+$labels['tabresources'] = 'Middelen';
+$labels['tabattachments'] = 'Toebehoren';
+$labels['tabsharing'] = 'Delen';
+$labels['deleteobjectconfirm'] = 'Weet je zeker dat je deze afspraak wilt verwijderen?';
+$labels['deleteventconfirm'] = 'Weet u zeker dat u deze afspraak wilt verwijderen?';
+$labels['deletecalendarconfirm'] = 'Weet u zeker dat u deze kalender samen met alle afspraken wilt verwijderen?';
+$labels['deletecalendarconfirmrecursive'] = 'Weet u zeker dat je deze kalender samen met alle afspraken en er onder hangende kalenders wilt verwijderen?';
+$labels['savingdata'] = 'Data wordt opgeslagen...';
+$labels['errorsaving'] = 'Opslaan mislukt.';
+$labels['operationfailed'] = 'De gevraagde opdracht is mislukt.';
+$labels['invalideventdates'] = 'Ongeldige datums ingevoerd! Controleer aub uw invoer.';
+$labels['invalidcalendarproperties'] = 'Ongeldige kalender eigenschappen! Gebruik aub een geldige naam.';
+$labels['searchnoresults'] = 'Geen afspraken gevonden in de geselecteerde kalenders.';
+$labels['successremoval'] = 'De afspraak is succesvol verwijdert.';
+$labels['successrestore'] = 'De afspraak is succesvol herstelt';
+$labels['errornotifying'] = 'Uitnodigingen versturen naar de genodigden van de afspraak is mislukt';
+$labels['errorimportingevent'] = 'Afspraak importeren is mislukt';
+$labels['importwarningexists'] = 'Een kopie van deze afspraak bevindt zich al in uw kalender.';
+$labels['newerversionexists'] = 'Een nieuwere versie van deze afspraak bestaat! Bewerking afgebroken.';
+$labels['nowritecalendarfound'] = 'Geen kalender gevonden om de afspraak in op te slaan';
+$labels['importedsuccessfully'] = 'De afspraak is succesvol toegevoegd aan \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Het bijwerken van de gebeurtenis in \'$calendar\' is geslaagd';
+$labels['attendeupdateesuccess'] = 'Status van genodigde succesvol gewijzigd';
+$labels['itipsendsuccess'] = 'Uitnodiging verstuurd aan de genodigden.';
+$labels['itipresponseerror'] = 'Antwoord op deze uitnodiging versturen is mislukt';
+$labels['itipinvalidrequest'] = 'Deze uitnodiging is niet langer geldig';
+$labels['sentresponseto'] = 'Antwoord op deze uitnodiging was succesvol verstuurd aan $mailto';
+$labels['localchangeswarning'] = 'U bent bezig wijzigingen te maken die alleen zichtbaar zijn in u eigen kalender en die niet doorgestuurd zullen worden aan de organisator van de afspraak.';
+$labels['importsuccess'] = 'Succesvol $nr afspraken geïmporteerd';
+$labels['importnone'] = 'Geen afspraken gevonden om te importeren';
+$labels['importerror'] = 'Er is een fout tijdens het importeren opgetreden';
+$labels['aclnorights'] = 'U heeft geen administratieve rechten op deze kalender.';
+$labels['changeeventconfirm'] = 'Wijzig afspraak';
+$labels['changerecurringeventwarning'] = 'Dit is een terugkerende afspraak. Wilt u alleen de huidige afspraak wijzigen, deze en alle toekomstige afspraken, alle afspraken of opslaan als een nieuwe afspraak?';
+$labels['currentevent'] = 'Huidige';
+$labels['futurevents'] = 'Toekomstige';
+$labels['allevents'] = 'Alle';
+$labels['saveasnew'] = 'Bewaar als nieuw';
+$labels['birthdays'] = 'Verjaardagen';
+$labels['birthdayscalendar'] = 'Verjaardagskalender';
+$labels['displaybirthdayscalendar'] = 'Toon verjaardagskalender';
+$labels['birthdayscalendarsources'] = 'Uit deze adresboeken';
+$labels['birthdayeventtitle'] = 'verjaardag van $name';
+$labels['birthdayage'] = 'Leeftijd $age';
+$labels['user'] = 'Gebruiker';
+$labels['actionappend'] = 'Opgeslagen';
+$labels['actionmove'] = 'Verplaatst';
+$labels['actiondelete'] = 'Verwijderd';
+$labels['compare'] = 'Vergelijken';
+?>
diff --git a/calendar/localization/pl_PL.inc b/calendar/localization/pl_PL.inc
new file mode 100644
index 0000000..7e6d5ac
--- /dev/null
+++ b/calendar/localization/pl_PL.inc
@@ -0,0 +1,272 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Domyślny widok';
+$labels['time_format'] = 'Format czasu';
+$labels['timeslots'] = 'Przedziały czasowe w ciągu godziny';
+$labels['first_day'] = 'Pierwszy dzień tygodnia';
+$labels['first_hour'] = 'Pierwsza godzina';
+$labels['workinghours'] = 'Godziny robocze';
+$labels['add_category'] = 'Dodaj kategorię';
+$labels['remove_category'] = 'Usuń kategorię';
+$labels['defaultcalendar'] = 'Twórz nowe zdarzenia w';
+$labels['eventcoloring'] = 'Kolor zdarzenia';
+$labels['coloringmode0'] = 'Zgodnie z kalendarzem';
+$labels['coloringmode1'] = 'Zgodnie z kategorią';
+$labels['coloringmode2'] = 'Kalendarz dla obramowania, kategoria dla środka';
+$labels['coloringmode3'] = 'Kategoria dla obramowania, kalendarz dla środka';
+$labels['afternothing'] = 'Nie rób nic';
+$labels['aftertrash'] = 'Przenieś do Kosza';
+$labels['afterdelete'] = 'Usuń wiadomość';
+$labels['afterflagdeleted'] = 'Oznacz jako usunięta';
+$labels['aftermoveto'] = 'Przenieś do...';
+$labels['itipoptions'] = 'Zaproszenia';
+$labels['afteraction'] = 'Wiadomość jest przetwarzana po zaproszeniu lub aktualizacji';
+$labels['calendar'] = 'Kalendarz';
+$labels['calendars'] = 'Kalendarze';
+$labels['category'] = 'Kategoria';
+$labels['categories'] = 'Kategorie';
+$labels['createcalendar'] = 'Utwórz nowy kalendarz';
+$labels['editcalendar'] = 'Edytuj właściwości kalendarza';
+$labels['name'] = 'Nazwa';
+$labels['color'] = 'Kolor';
+$labels['day'] = 'Dzień';
+$labels['week'] = 'Tydzień';
+$labels['month'] = 'Miesiąc';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nowy';
+$labels['new_event'] = 'Nowe zdarzenie';
+$labels['edit_event'] = 'Edytuj zdarzenie';
+$labels['edit'] = 'Edytuj';
+$labels['save'] = 'Zapisz';
+$labels['removelist'] = 'Usuń z listy';
+$labels['cancel'] = 'Anuluj';
+$labels['select'] = 'Wybierz';
+$labels['print'] = 'Drukuj';
+$labels['printtitle'] = 'Drukuj kalendarze';
+$labels['title'] = 'Podsumowanie';
+$labels['description'] = 'Opis';
+$labels['all-day'] = 'cały dzień';
+$labels['export'] = 'Eksport';
+$labels['exporttitle'] = 'Eksport w formacie iCalendar';
+$labels['exportrange'] = 'Zdarzenia z';
+$labels['exportattachments'] = 'Z załącznikami';
+$labels['customdate'] = 'Własna data';
+$labels['location'] = 'Położenie';
+$labels['url'] = 'Adres URL';
+$labels['date'] = 'Data';
+$labels['start'] = 'Początek';
+$labels['starttime'] = 'Początek';
+$labels['end'] = 'Koniec';
+$labels['endtime'] = 'Koniec';
+$labels['repeat'] = 'Powtórz';
+$labels['selectdate'] = 'Wybierz datę';
+$labels['freebusy'] = 'Pokaż mnie jako';
+$labels['free'] = 'Wolny';
+$labels['busy'] = 'Zajęty';
+$labels['outofoffice'] = 'Poza biurem';
+$labels['tentative'] = 'Niepewny';
+$labels['mystatus'] = 'Mój status';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Potwierdzony';
+$labels['status-cancelled'] = 'Anulowany';
+$labels['priority'] = 'Priorytet';
+$labels['sensitivity'] = 'Poufność';
+$labels['public'] = 'publiczny';
+$labels['private'] = 'prywatny';
+$labels['confidential'] = 'poufny';
+$labels['links'] = 'Reference';
+$labels['alarms'] = 'Przypomnienie';
+$labels['comment'] = 'Komentarz';
+$labels['created'] = 'Utworzono';
+$labels['changed'] = 'Ostatnia modyfikacja';
+$labels['unknown'] = 'Nieznany';
+$labels['eventoptions'] = 'Opcje';
+$labels['generated'] = 'wygenerowano';
+$labels['eventhistory'] = 'Historia';
+$labels['removelink'] = 'Usuń odnośnik e-mail';
+$labels['printdescriptions'] = 'Drukuj opisy';
+$labels['parentcalendar'] = 'Wstaw wewnątrz';
+$labels['searchearlierdates'] = '« Szukaj wcześniejszych zdarzeń';
+$labels['searchlaterdates'] = 'Szukaj późniejszych zdarzeń »';
+$labels['andnmore'] = '$nr więcej...';
+$labels['togglerole'] = 'Kliknuj aby przestawić rolę';
+$labels['createfrommail'] = 'Zapisz jako zdarzenie';
+$labels['importevents'] = 'Importuj zdarzenia';
+$labels['importrange'] = 'Zdarzenia z';
+$labels['onemonthback'] = '1 miesiąc wstecz';
+$labels['nmonthsback'] = '$nr miesięcy wstecz';
+$labels['showurl'] = 'Pokaż adres URL kalendarza';
+$labels['showurldescription'] = 'Używaj tego adresu aby dostać się do kalendarza z innych programów (w trybie tylko-do-odczytu). Możesz wkleić go do dowolnej aplikacji obsługującej format iCal.';
+$labels['caldavurldescription'] = 'Skopiuj ten adres do aplikacji obsługującej format <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> (np. Evolution lub Mozilla Thunderbird) aby zsynchronizować wybrany kalendarz z twoim komputerem lub urządzeniem przenośnym.';
+$labels['findcalendars'] = 'Wyszukaj kalendarze...';
+$labels['searchterms'] = 'Szukana fraza';
+$labels['calsearchresults'] = 'Dostępne kalendarze';
+$labels['calendarsubscribe'] = 'Dodaj do listy na stałe';
+$labels['nocalendarsfound'] = 'Nie znaleziono żadych kalendarzy';
+$labels['nrcalendarsfound'] = 'znaleziono $nr kalendarzy';
+$labels['quickview'] = 'Pokaż tylko ten kalendarz';
+$labels['invitationspending'] = 'Oczekujące zaproszenia';
+$labels['invitationsdeclined'] = 'Odrzucone zaproszenia';
+$labels['changepartstat'] = 'Zmień status uczestnika';
+$labels['rsvpcomment'] = 'Treść zaproszenia';
+$labels['listrange'] = 'Zakres do pokazania:';
+$labels['listsections'] = 'Podziel na:';
+$labels['smartsections'] = 'Inteligentne sekcje';
+$labels['until'] = 'dopóki';
+$labels['today'] = 'Dzisiaj';
+$labels['tomorrow'] = 'Jutro';
+$labels['thisweek'] = 'Bieżący tydzień';
+$labels['nextweek'] = 'Następny tydzień';
+$labels['prevweek'] = 'Poprzedni tydzień';
+$labels['thismonth'] = 'Bieżący miesiąc';
+$labels['nextmonth'] = 'Następny miesiąc';
+$labels['weekofyear'] = 'Tydzień';
+$labels['pastevents'] = 'Przeszłe';
+$labels['futureevents'] = 'Przyszłe';
+$labels['showalarms'] = 'Pokaż powiadomienia';
+$labels['defaultalarmtype'] = 'Domyślne powiadomienie';
+$labels['defaultalarmoffset'] = 'Domyślny czas powiadomienia';
+$labels['attendee'] = 'Uczestnik';
+$labels['role'] = 'Rola';
+$labels['availability'] = 'Dostępny';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Dodaj uczestnika';
+$labels['roleorganizer'] = 'Organizator';
+$labels['rolerequired'] = 'Wymagany';
+$labels['roleoptional'] = 'Opcjonalny';
+$labels['rolechair'] = 'Przewodniczący';
+$labels['rolenonparticipant'] = 'Nieobecny';
+$labels['cutypeindividual'] = 'Osoba';
+$labels['cutypegroup'] = 'Grupa';
+$labels['cutyperesource'] = 'Zasób';
+$labels['cutyperoom'] = 'Pokój';
+$labels['availfree'] = 'Wolny';
+$labels['availbusy'] = 'Zajęty';
+$labels['availunknown'] = 'Nieznany';
+$labels['availtentative'] = 'Niepewny';
+$labels['availoutofoffice'] = 'Poza biurem';
+$labels['delegatedto'] = 'Oddelegowany do:';
+$labels['delegatedfrom'] = 'Oddelegowany z:';
+$labels['scheduletime'] = 'Sprawdź dostępność';
+$labels['sendinvitations'] = 'Wyślij zaproszenia';
+$labels['sendnotifications'] = 'Powiadom uczestników o zmianach';
+$labels['sendcancellation'] = 'Powiadom uczestników o anulowaniu zdarzenia';
+$labels['onlyworkinghours'] = 'Sprawdź dostępność w moich godzinach pracy';
+$labels['reqallattendees'] = 'Wymagany/wszyscy uczestnicy';
+$labels['prevslot'] = 'Poprzedni przedział';
+$labels['nextslot'] = 'Następny przedział';
+$labels['suggestedslot'] = 'Sugerowany przedział';
+$labels['noslotfound'] = 'Nie znaleziono wolnego przedziału czasu';
+$labels['invitationsubject'] = 'Zostałeś zaproszony do "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nKiedy: \$date\n\nZaproszeni: \$attendees\n\nW załączeniu plik w formacie iCalendar ze szczegółami zdarzenia, który możesz zaimportować do twojej aplikacji kalendarza.";
+$labels['invitationattendlinks'] = "W przypadku gdy klient poczty elektronicznej nie obsługuje rządań w formacie iTip, aby zaakceptować lub odrzucić to zaproszenie, można skorzystać z następującego linku:\n\$url ";
+$labels['eventupdatesubject'] = '"$title" zostało zaktualizowane';
+$labels['eventupdatesubjectempty'] = 'Zdarzenie które cię dotyczy zostało zaktualizowane';
+$labels['eventupdatemailbody'] = "*\$title*\n\nKiedy: \$date\n\nZaproszeni: \$attendees\n\nW załączeniu plik w formacie iCalendar zawierający zaktualizowane szczegóły zdarzenia, które możesz zaimportować do swojej aplikacji kalendarza.";
+$labels['eventcancelsubject'] = '"$title" zostało anulowane';
+$labels['eventcancelmailbody'] = "*\$title*\n\nKiedy: \$date\n\nZaproszeni: \$attendees\n\nZdarzenie zostało anulowane przez \$organizer.\n\nW załączeniu plik w formacie iCalendar ze zaktualizowanymi szczegółami zdarzenia.";
+$labels['itipobjectnotfound'] = 'W twoim kalendarzu nie znaleziono zdarzenia związanego z tą wiadomością.';
+$labels['itipmailbodyaccepted'] = "\$sender zaakceptował zaproszenie do następującego zdarzenia:\n\n*\$title*\n\nKiedy: \$date\n\nZaproszeni: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender warunkowo zaakceptował zaproszenie do następującego zdarzenia:\n\n*\$title*\n\nKiedy: \$date\n\nZaproszeni: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender odrzucił zaproszenie na następujące zdarzenie:\n\n*\$title*\n\nKiedy: \$date\n\nZaproszeni: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender odrzucił twój udział w zastępującym zdarzeniu:\n\n*\$title*\n\nKiedy: \$date";
+$labels['itipdeclineevent'] = 'Czy chcesz odrzucić zaproszenie na to zdarzenie?';
+$labels['declinedeleteconfirm'] = 'Czy chcesz także usunąć to odrzucone zdarzenie ze swojego kalendarza?';
+$labels['itipcomment'] = 'Komentarz zaproszenia/powiadomienia';
+$labels['itipcommenttitle'] = 'Komentarz ten będzie dołączony do wiadomości wysłanej do uczestników zdarzenia';
+$labels['notanattendee'] = 'Nie jesteś na liście uczestników tego zdarzenia';
+$labels['eventcancelled'] = 'Zdarzenie zostało anulowane';
+$labels['saveincalendar'] = 'zapisz w';
+$labels['updatemycopy'] = 'Uaktualnij w moim kalendarzu';
+$labels['savetocalendar'] = 'Zapisz do kalendarza';
+$labels['openpreview'] = 'Sprawdź kalendarz';
+$labels['noearlierevents'] = 'Brak wcześniejszych zdarzeń';
+$labels['nolaterevents'] = '« Brak póżniejszych zdarzeń';
+$labels['resource'] = 'Zasób';
+$labels['addresource'] = 'Rezerwuj zasób';
+$labels['findresources'] = 'Wyszukaj zasoby';
+$labels['resourcedetails'] = 'Szczegóły';
+$labels['resourceavailability'] = 'Dostępność';
+$labels['resourceowner'] = 'Właściciel';
+$labels['resourceadded'] = 'Zasób został dodany do twojego zdarzenia';
+$labels['tabsummary'] = 'Podsumowanie';
+$labels['tabrecurrence'] = 'Powtarzalność';
+$labels['tabattendees'] = 'Uczestnicy';
+$labels['tabresources'] = 'Zasoby';
+$labels['tabattachments'] = 'Załączniki';
+$labels['tabsharing'] = 'Udostępnianie';
+$labels['deleteobjectconfirm'] = 'Czy na pewno chcesz usunąć to zdarzenie?';
+$labels['deleteventconfirm'] = 'Czy na pewno chcesz usunąć to zdarzenie?';
+$labels['deletecalendarconfirm'] = 'Czy na pewno chcesz usunąć ten kalendarz z wszystkimi zadaniami?';
+$labels['deletecalendarconfirmrecursive'] = 'Czy na pewno chcesz usunąć ten kalendarz ze wszystkimi zdarzeniami i pod-kalendarzami?';
+$labels['savingdata'] = 'Zapisuję dane...';
+$labels['errorsaving'] = 'Błąd podczas zapisu danych.';
+$labels['operationfailed'] = 'Żądana operacja nie powiodła się.';
+$labels['invalideventdates'] = 'Błędna data! Proszę sprawdzić wprowadzone dane.';
+$labels['invalidcalendarproperties'] = 'Błędna właściwość kalendarza! Proszę podać poprawną nazwę.';
+$labels['searchnoresults'] = 'Nie znaleziono zdarzeń w wybranym kalendarzu.';
+$labels['successremoval'] = 'Zdarzenie zostało usunięte.';
+$labels['successrestore'] = 'Zdarzenie zostało przywrócone.';
+$labels['errornotifying'] = 'Nie udało się wysłać powiadomień do uczestników zdarzenia';
+$labels['errorimportingevent'] = 'Nie udało się zaimportować zdarzenia';
+$labels['importwarningexists'] = 'Kopia tego zdarzenia już istnieje w twoim kalendarzu.';
+$labels['newerversionexists'] = 'Istnieje nowsza wersja tego zdarzenia ! Przerwano.';
+$labels['nowritecalendarfound'] = 'Nie znaleziono kalendarza aby zapisać zdarzenie.';
+$labels['importedsuccessfully'] = 'Zdarzenie dodano do \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Zdarzenie zostało pomyślnie zaktualizowane w \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Zaktualizowano status uczestnika.';
+$labels['itipsendsuccess'] = 'Wysłano zaproszenia do uczestników.';
+$labels['itipresponseerror'] = 'Nie udało się wysłać odpowiedzi na to zaproszenie.';
+$labels['itipinvalidrequest'] = 'To zaproszenie nie jest już aktualne.';
+$labels['sentresponseto'] = 'Wysłano odpowiedź na zaproszenie do $mailto.';
+$labels['localchangeswarning'] = 'Zamierzasz dokonać zmian, które mogą zostać wykonane tylko w twoim kalendarzu i nie zostaną wysłane do organizatora zdarzenia.';
+$labels['importsuccess'] = 'Zaimportowano $nr zdarzeń.';
+$labels['importnone'] = 'Nie znaleziono zdarzeń do zaimportowania.';
+$labels['importerror'] = 'Wystąpił błąd podczas importu.';
+$labels['aclnorights'] = 'Nie masz uprawnień administracyjnych dla tego kalendarza.';
+$labels['changeeventconfirm'] = 'Zmień zdarzenie';
+$labels['removeeventconfirm'] = 'Usuń zdarzenie';
+$labels['changerecurringeventwarning'] = 'To jest zdarzenie powtarzalne. Czy chcesz zmienić bieżące zdarzenie, bieżące i przyszłe, wszystkie, a może zapisać je jako nowe zdarzenie?';
+$labels['removerecurringeventwarning'] = 'Jest to zdarzenie cykliczne. Czy chcesz usunąć wyłącznie bieżące zdarzenie i jego przyszłe wystąpienia, czy wszystkie wystąpienia tego zdarzenia?';
+$labels['removerecurringallonly'] = 'Jest to zdarzenie cykliczne. Jako uczestnik, możesz jedynie usunąć całe zdarzenie ze wszystkimi jego wystąpieniami.';
+$labels['currentevent'] = 'Bieżące';
+$labels['futurevents'] = 'Przyszłe';
+$labels['allevents'] = 'Wszystkie';
+$labels['saveasnew'] = 'Zapisz jako nowe';
+$labels['birthdays'] = 'Uruodziny';
+$labels['birthdayscalendar'] = 'Kalendarz Urodzin';
+$labels['displaybirthdayscalendar'] = 'Wyświetl kalendarz urodzin';
+$labels['birthdayscalendarsources'] = 'Z tych książek adresowych';
+$labels['birthdayeventtitle'] = 'Urodziny $name\'s';
+$labels['birthdayage'] = 'Wiek $age';
+$labels['objectchangelog'] = 'Historia zmian';
+$labels['revision'] = 'Wersja';
+$labels['user'] = 'Użytkownik';
+$labels['operation'] = 'Akcja';
+$labels['actionappend'] = 'Zapisane';
+$labels['actionmove'] = 'Przeniesione';
+$labels['actiondelete'] = 'Usunięte';
+$labels['compare'] = 'Porównaj';
+$labels['showrevision'] = 'Pokaż tą wersję';
+$labels['restore'] = 'Przywróć tą wersję';
+$labels['objectnotfound'] = 'Nie udało się wczytać zdarzenia';
+$labels['objectchangelognotavailable'] = 'Historia zmian jest niedostępna dla tego zdarzenia';
+$labels['objectdiffnotavailable'] = 'Nie można porównać wybranych wersji';
+$labels['revisionrestoreconfirm'] = 'Czy na pewno chcesz przywrócić wersję $rev tego zdarzenia? Bierzące zdarzenie zostanie zastąpione starszą wersją.';
+$labels['arialabelminical'] = 'Wybór daty kalendarza';
+$labels['arialabelcalendarview'] = 'Podgląd kalendarza';
+$labels['arialabelsearchform'] = 'Formularz wyszukiwania zdarzeń';
+$labels['arialabelquicksearchbox'] = 'Fraza wyszukiwania zdarzeń';
+$labels['arialabelcalsearchform'] = 'Formularz wyszukiwania kalendarzy';
+$labels['calendaractions'] = 'Akcje kalendarzy';
+$labels['arialabeleventattendees'] = 'Lista uczestników zdarzenia';
+$labels['arialabeleventresources'] = 'Lista zasobów zdarzenia';
+$labels['arialabelresourcesearchform'] = 'Formularz wyszukiwania zasobów';
+$labels['arialabelresourceselection'] = 'Dostępne zasoby';
+?>
diff --git a/calendar/localization/pt_BR.inc b/calendar/localization/pt_BR.inc
new file mode 100644
index 0000000..62efbdf
--- /dev/null
+++ b/calendar/localization/pt_BR.inc
@@ -0,0 +1,213 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Visualização padrão';
+$labels['time_format'] = 'Formato da hora';
+$labels['timeslots'] = 'Time slots per hour';
+$labels['first_day'] = 'Primeiro dia da semana';
+$labels['first_hour'] = 'Primeira hora a mostrar';
+$labels['workinghours'] = 'Horário de trabalho';
+$labels['add_category'] = 'Adicionar categoria';
+$labels['remove_category'] = 'Remover categoria';
+$labels['defaultcalendar'] = 'Criar novos eventos em';
+$labels['eventcoloring'] = 'Coloração de evento';
+$labels['coloringmode0'] = 'De acordo com o calendário';
+$labels['coloringmode1'] = 'De acordo com a categoria';
+$labels['coloringmode2'] = 'Calendário para esboço, categoria para conteúdo';
+$labels['coloringmode3'] = 'Categoria para esboço, calendário para conteúdo';
+$labels['calendar'] = 'Calendário';
+$labels['calendars'] = 'Calendários';
+$labels['category'] = 'Categoria';
+$labels['categories'] = 'Categorias';
+$labels['createcalendar'] = 'Criar novo calendário';
+$labels['editcalendar'] = 'Editar propriedades do calendário';
+$labels['name'] = 'Nome';
+$labels['color'] = 'Cor';
+$labels['day'] = 'Dia';
+$labels['week'] = 'Semana';
+$labels['month'] = 'Mês';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Novo';
+$labels['new_event'] = 'Novo evento';
+$labels['edit_event'] = 'Editar evento';
+$labels['edit'] = 'Editar';
+$labels['save'] = 'Gravar';
+$labels['cancel'] = 'Cancelar';
+$labels['select'] = 'Selecionar';
+$labels['print'] = 'Imprimir';
+$labels['printtitle'] = 'Imprimir calendários';
+$labels['title'] = 'Sumário';
+$labels['description'] = 'Descrição';
+$labels['all-day'] = 'dia todo';
+$labels['export'] = 'Exportar';
+$labels['exporttitle'] = 'Exportar para iCalendar';
+$labels['exportrange'] = 'Eventos de';
+$labels['exportattachments'] = 'With attachments';
+$labels['customdate'] = 'Custom date';
+$labels['location'] = 'Local';
+$labels['date'] = 'Data';
+$labels['start'] = 'Início';
+$labels['end'] = 'Término';
+$labels['repeat'] = 'Repetir';
+$labels['selectdate'] = 'Escolha a data';
+$labels['freebusy'] = 'Mostrar me como';
+$labels['free'] = 'Disponível';
+$labels['busy'] = 'Ocupado';
+$labels['outofoffice'] = 'Fora de escritório';
+$labels['tentative'] = 'Tentativa';
+$labels['status'] = 'Situação';
+$labels['status-confirmed'] = 'Confirmado';
+$labels['status-cancelled'] = 'Cancalado';
+$labels['priority'] = 'Prioridade';
+$labels['sensitivity'] = 'Privacidade';
+$labels['public'] = 'público';
+$labels['private'] = 'privado';
+$labels['confidential'] = 'confidencial';
+$labels['links'] = 'Reference';
+$labels['alarms'] = 'Lembrete';
+$labels['comment'] = 'Comentário';
+$labels['unknown'] = 'Desconhecido';
+$labels['eventoptions'] = 'Opções';
+$labels['generated'] = 'gerado em';
+$labels['printdescriptions'] = 'Descrições de impressão';
+$labels['parentcalendar'] = 'Inserir dentro';
+$labels['searchearlierdates'] = '« Procurar por eventos anteriores';
+$labels['searchlaterdates'] = 'Procurar por eventos posteriores »';
+$labels['andnmore'] = '$nr mais...';
+$labels['togglerole'] = 'Clique para alternar o papel';
+$labels['createfrommail'] = 'Salvar como evento';
+$labels['importevents'] = 'Importar eventos';
+$labels['importrange'] = 'Eventos de';
+$labels['onemonthback'] = '1 mês atrás';
+$labels['nmonthsback'] = '$nr meses atrás';
+$labels['showurl'] = 'Mostrar URL do calendário';
+$labels['showurldescription'] = 'Use o seguinte endereço para acessar (somente leitura) seu calendário em outras aplicações. Você pode copiar e colar este endereço em qualquer software de calendário que suporte o formato iCal.';
+$labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
+$labels['findcalendars'] = 'Buscar calendários...';
+$labels['searchterms'] = 'Search terms';
+$labels['calsearchresults'] = 'Calendários disponíveis';
+$labels['calendarsubscribe'] = 'Listar permanentemente';
+$labels['listrange'] = 'Intervalo para exibir:';
+$labels['listsections'] = 'Dividir em:';
+$labels['smartsections'] = 'Seções inteligentes';
+$labels['until'] = 'até';
+$labels['today'] = 'Hoje';
+$labels['tomorrow'] = 'Amanhã';
+$labels['thisweek'] = 'Esta semana';
+$labels['nextweek'] = 'Próxima semana';
+$labels['thismonth'] = 'Este mês';
+$labels['nextmonth'] = 'Próximo mês';
+$labels['weekofyear'] = 'Semana';
+$labels['pastevents'] = 'Passado';
+$labels['futureevents'] = 'Futuro';
+$labels['defaultalarmtype'] = 'Configuração de lembrete padrão';
+$labels['defaultalarmoffset'] = 'Horário padrão de lembrete';
+$labels['attendee'] = 'Participante';
+$labels['role'] = 'Papel';
+$labels['availability'] = 'Disp.';
+$labels['confirmstate'] = 'Situação';
+$labels['addattendee'] = 'Adicionar participante';
+$labels['roleorganizer'] = 'Organizador';
+$labels['rolerequired'] = 'Obrigatório';
+$labels['roleoptional'] = 'Opcional';
+$labels['cutypegroup'] = 'Grupo';
+$labels['cutyperesource'] = 'Recurso';
+$labels['availfree'] = 'Disponível';
+$labels['availbusy'] = 'Ocupado';
+$labels['availunknown'] = 'Desconhecido';
+$labels['availtentative'] = 'Tentativa';
+$labels['availoutofoffice'] = 'Fora de escritório';
+$labels['delegatedto'] = 'Delegado para:';
+$labels['delegatedfrom'] = 'Delegado de:';
+$labels['scheduletime'] = 'Procurar disponibilidade';
+$labels['sendinvitations'] = 'Enviar convites';
+$labels['sendnotifications'] = 'Avisar os participantes sobre as modificações';
+$labels['sendcancellation'] = 'Avisar os participantes sobre o cancelamento do evento';
+$labels['onlyworkinghours'] = 'Procurar disponibilidade dentro do meu horário de trabalho';
+$labels['reqallattendees'] = 'Obrigatório/todos os participantes';
+$labels['prevslot'] = 'Espaço anterior';
+$labels['nextslot'] = 'Próximo espaço';
+$labels['noslotfound'] = 'Incapaz de encontrar um horário disponível';
+$labels['invitationsubject'] = 'Você foi convidado para "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nSegue em anexo um arquivo iCalendar com todos os detalhes do evento na qual você pode importar para sua aplicação de calendário.";
+$labels['invitationattendlinks'] = "No caso do seu cliente de e-mail não suportar requisições iTIP você pode usar o link a seguir para aceitar ou recusar este convite:\n\$url";
+$labels['eventupdatesubject'] = '"$title" foi atualizado';
+$labels['eventupdatesubjectempty'] = 'Um evento do seu interesse foi atualizado';
+$labels['eventupdatemailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nSegue em anexo um arquivo iCalendar com os detalhes atualizados do evento na qual você pode importar para sua aplicação de calendário.";
+$labels['eventcancelsubject'] = '"$title" foi cancelado';
+$labels['eventcancelmailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nO evento foi cancelado por \$organizer.\n\nSegue em anexo um arquivo iCalendar com os detalhes atualizados do evento.";
+$labels['itipobjectnotfound'] = 'O evento referenciado por esta mensagem não foi encontrado em seu calendário.';
+$labels['itipmailbodyaccepted'] = "\$sender aceitou o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender aceitou como tentativa o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender recusou o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender rejeitou sua participação no seguinte evento:\n\n*\$title*\n\nQuando: \$date";
+$labels['itipdeclineevent'] = 'Você deseja recusar o convite para este evento?';
+$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['notanattendee'] = 'Você não está listado como um participante deste evento';
+$labels['eventcancelled'] = 'O evento foi cancelado';
+$labels['saveincalendar'] = 'salvar em';
+$labels['updatemycopy'] = 'Atualize em meu calendário';
+$labels['savetocalendar'] = 'Gravar no calendário';
+$labels['resource'] = 'Recurso';
+$labels['addresource'] = 'Livro de recursos';
+$labels['findresources'] = 'Encontrar recursos';
+$labels['resourcedetails'] = 'Detalhes';
+$labels['resourceavailability'] = 'Disponibilidade';
+$labels['resourceowner'] = 'Dono';
+$labels['resourceadded'] = 'O recurso foi adicionado ao seu evento';
+$labels['tabsummary'] = 'Sumário';
+$labels['tabrecurrence'] = 'Repetição';
+$labels['tabattendees'] = 'Participantes';
+$labels['tabresources'] = 'Recursos';
+$labels['tabattachments'] = 'Anexos';
+$labels['tabsharing'] = 'Compartilhamento';
+$labels['deleteobjectconfirm'] = 'Você realmente deseja remover este evento?';
+$labels['deleteventconfirm'] = 'Você realmente deseja remover este evento?';
+$labels['deletecalendarconfirm'] = 'Você realmente deseja excluir este calendário com todos os seus eventos?';
+$labels['deletecalendarconfirmrecursive'] = 'Do you really want to delete this calendar with all its events and sub-calendars?';
+$labels['savingdata'] = 'Salvando dados...';
+$labels['errorsaving'] = 'Falha ao salvar as modificações.';
+$labels['operationfailed'] = 'A operação requisitada falhou.';
+$labels['invalideventdates'] = 'Datas inválidas inseridas! Por favor verifique a inserção.';
+$labels['invalidcalendarproperties'] = 'Propriedades de calendário inválidas! Por favor defina um nome válido.';
+$labels['searchnoresults'] = 'Nenhum evento encontrado nos calendários selecionados.';
+$labels['successremoval'] = 'O evento foi excluído com sucesso.';
+$labels['successrestore'] = 'O evento foi restaurado com sucesso.';
+$labels['errornotifying'] = 'Falha ao enviar notificações para os participantes do evento.';
+$labels['errorimportingevent'] = 'Falha ao importar evento';
+$labels['importwarningexists'] = 'Uma cópia deste evento já existe em seu calendário.';
+$labels['newerversionexists'] = 'Já existe uma nova versão deste evento! Abortado.';
+$labels['nowritecalendarfound'] = 'Nenhum calendário encontrado para salvar o evento';
+$labels['importedsuccessfully'] = 'O evento foi adicionado com sucesso em \'$calendar\'';
+$labels['updatedsuccessfully'] = 'O evento foi atualizado com sucesso em \'$calendar\'.';
+$labels['attendeupdateesuccess'] = 'O status do participante foi atualizado com sucesso.';
+$labels['itipsendsuccess'] = 'Convite enviado aos participantes.';
+$labels['itipresponseerror'] = 'Falha ao enviar a resposta para este convite de evento';
+$labels['itipinvalidrequest'] = 'Este convite não é mais válido';
+$labels['sentresponseto'] = 'Resposta de convite enviada com sucesso para $mailto';
+$labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your calendar and not be sent to the organizer of the event.';
+$labels['importsuccess'] = 'Importado com sucesso $nr eventos';
+$labels['importnone'] = 'Não há eventos a serem importados';
+$labels['importerror'] = 'Ocorreu um erro na importação';
+$labels['aclnorights'] = 'Você não tem permissão de administrador neste calendário.';
+$labels['changeeventconfirm'] = 'Trocar evento';
+$labels['changerecurringeventwarning'] = 'Este é um evento com repetição. Você gostaria de editar o evento atual somente, estas e todas as futuras ocorrências ou salvar este como um novo evento?';
+$labels['currentevent'] = 'Atual';
+$labels['futurevents'] = 'Futuro';
+$labels['allevents'] = 'Todos';
+$labels['saveasnew'] = 'Salvar como novo';
+$labels['birthdays'] = 'Birthdays';
+$labels['birthdayscalendar'] = 'Birthdays Calendar';
+$labels['displaybirthdayscalendar'] = 'Display birthdays calendar';
+$labels['birthdayscalendarsources'] = 'From these address books';
+$labels['birthdayeventtitle'] = '$name\'s Birthday';
+$labels['birthdayage'] = 'Age $age';
+$labels['user'] = 'Usuário';
+$labels['actiondelete'] = 'Deletado';
+?>
diff --git a/calendar/localization/pt_PT.inc b/calendar/localization/pt_PT.inc
new file mode 100644
index 0000000..d6b38ab
--- /dev/null
+++ b/calendar/localization/pt_PT.inc
@@ -0,0 +1,274 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Visualização padrão';
+$labels['time_format'] = 'Formato da hora';
+$labels['timeslots'] = 'Entradas por hora';
+$labels['first_day'] = 'Primeiro dia da semana';
+$labels['first_hour'] = 'Primeira hora a mostrar';
+$labels['workinghours'] = 'Horário de trabalho';
+$labels['add_category'] = 'Adicionar categoria';
+$labels['remove_category'] = 'Remover categoria';
+$labels['defaultcalendar'] = 'Criar novos eventos em';
+$labels['eventcoloring'] = 'Cores dos eventos';
+$labels['coloringmode0'] = 'De acordo com o calendário';
+$labels['coloringmode1'] = 'De acordo com a categoria';
+$labels['coloringmode2'] = 'Calendário para esboço, categoria para conteúdo';
+$labels['coloringmode3'] = 'Categoria para esboço, calendário para conteúdo';
+$labels['afternothing'] = 'Manter';
+$labels['aftertrash'] = 'Enviar para o lixo';
+$labels['afterdelete'] = 'Eliminar a mensagem';
+$labels['afterflagdeleted'] = 'Marcar como eliminada';
+$labels['aftermoveto'] = 'Mover para...';
+$labels['itipoptions'] = 'Convites de eventos';
+$labels['afteraction'] = 'Depois do processamento de um convite ou mensagem de alteração';
+$labels['calendar'] = 'Calendário';
+$labels['calendars'] = 'Calendários';
+$labels['category'] = 'Categoria';
+$labels['categories'] = 'Categorias';
+$labels['createcalendar'] = 'Criar um novo calendário';
+$labels['editcalendar'] = 'Alterar propriedades do calendário';
+$labels['name'] = 'Nome';
+$labels['color'] = 'Cor';
+$labels['day'] = 'Dia';
+$labels['week'] = 'Semana';
+$labels['month'] = 'Mês';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Novo';
+$labels['new_event'] = 'Novo evento';
+$labels['edit_event'] = 'Alterar evento';
+$labels['edit'] = 'Alterar';
+$labels['save'] = 'Guardar';
+$labels['removelist'] = 'Remover da lista';
+$labels['cancel'] = 'Cancelar';
+$labels['select'] = 'Selecionar';
+$labels['print'] = 'Imprimir';
+$labels['printtitle'] = 'Imprimir calendários';
+$labels['title'] = 'Sumário';
+$labels['description'] = 'Descrição';
+$labels['all-day'] = 'dia todo';
+$labels['export'] = 'Exportar';
+$labels['exporttitle'] = 'Exportar para iCalendar';
+$labels['exportrange'] = 'Eventos de';
+$labels['exportattachments'] = 'Incluir anexos';
+$labels['customdate'] = 'Definir data';
+$labels['location'] = 'Local';
+$labels['url'] = 'Endereço web';
+$labels['date'] = 'Data';
+$labels['start'] = 'Início';
+$labels['starttime'] = 'Hora de início';
+$labels['end'] = 'Fim';
+$labels['endtime'] = 'Hora de fim';
+$labels['repeat'] = 'Repetir';
+$labels['selectdate'] = 'Escolher data';
+$labels['freebusy'] = 'Mostrar-me como';
+$labels['free'] = 'Livre';
+$labels['busy'] = 'Ocupado';
+$labels['outofoffice'] = 'Ausente';
+$labels['tentative'] = 'Tentativa';
+$labels['mystatus'] = 'O meu estado';
+$labels['status'] = 'Estado';
+$labels['status-confirmed'] = 'Confirmado';
+$labels['status-cancelled'] = 'Cancelado';
+$labels['priority'] = 'Prioridade';
+$labels['sensitivity'] = 'Privacidade';
+$labels['public'] = 'público';
+$labels['private'] = 'privado';
+$labels['confidential'] = 'confidencial';
+$labels['links'] = 'Referência';
+$labels['alarms'] = 'Lembrete';
+$labels['comment'] = 'Comentário';
+$labels['created'] = 'Criado em';
+$labels['changed'] = 'Alterado em';
+$labels['unknown'] = 'Desconhecido';
+$labels['eventoptions'] = 'Opções';
+$labels['generated'] = 'produzido a';
+$labels['eventhistory'] = 'Histórico';
+$labels['removelink'] = 'Remover referência de email ';
+$labels['printdescriptions'] = 'Descrições de impressão';
+$labels['parentcalendar'] = 'Inserir dentro';
+$labels['searchearlierdates'] = '« Procurar eventos anteriores';
+$labels['searchlaterdates'] = 'Procurar eventos posteriores »';
+$labels['andnmore'] = '$nr mais...';
+$labels['togglerole'] = 'Clique para alternar o papel';
+$labels['createfrommail'] = 'Salvar como evento';
+$labels['importevents'] = 'Importar eventos';
+$labels['importrange'] = 'Eventos de';
+$labels['onemonthback'] = '1 mês atrás';
+$labels['nmonthsback'] = '$nr meses atrás';
+$labels['showurl'] = 'Mostrar URL do calendário';
+$labels['showurldescription'] = 'Use o seguinte endereço para obter acesso (somente leitura) ao seu calendário com outras aplicações. Para isso pode copiar e colar este endereço em qualquer software que suporte o formato iCal.';
+$labels['caldavurldescription'] = 'Para sincronizar este calendário com o seu computador ou dispositivos móveis deverá copiar este endereço <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> para a aplicação cliente. (ex. Evolution ou Mozilla Thunderbird)';
+$labels['findcalendars'] = 'Procurar calendários...';
+$labels['searchterms'] = 'Procurar termos';
+$labels['calsearchresults'] = 'Calendários disponíveis';
+$labels['calendarsubscribe'] = 'Listar sempre';
+$labels['nocalendarsfound'] = 'Não foram encontrados calendários';
+$labels['nrcalendarsfound'] = '$nr calendários encontrados';
+$labels['quickview'] = 'Só mostrar este calendário';
+$labels['invitationspending'] = 'Convites pendentes';
+$labels['invitationsdeclined'] = 'Convites recusados';
+$labels['changepartstat'] = 'Mudar estado de participante';
+$labels['rsvpcomment'] = 'Mensagem de convite';
+$labels['listrange'] = 'Intervalo para exibir:';
+$labels['listsections'] = 'Dividir em:';
+$labels['smartsections'] = 'Seções inteligentes';
+$labels['until'] = 'até';
+$labels['today'] = 'Hoje';
+$labels['tomorrow'] = 'Amanhã';
+$labels['thisweek'] = 'Esta semana';
+$labels['nextweek'] = 'Próxima semana';
+$labels['prevweek'] = 'Semana anterior';
+$labels['thismonth'] = 'Este mês';
+$labels['nextmonth'] = 'Próximo mês';
+$labels['weekofyear'] = 'Semana';
+$labels['pastevents'] = 'Passado';
+$labels['futureevents'] = 'Futuro';
+$labels['showalarms'] = 'Mostrar lembretes';
+$labels['defaultalarmtype'] = 'Configuração padrão de lembrete';
+$labels['defaultalarmoffset'] = 'Horário padrão de lembrete';
+$labels['attendee'] = 'Participante';
+$labels['role'] = 'Papel';
+$labels['availability'] = 'Disp.';
+$labels['confirmstate'] = 'Estado';
+$labels['addattendee'] = 'Adicionar participante';
+$labels['roleorganizer'] = 'Organizador';
+$labels['rolerequired'] = 'Obrigatório';
+$labels['roleoptional'] = 'Facultativo';
+$labels['rolechair'] = 'Responsável';
+$labels['rolenonparticipant'] = 'Ausente';
+$labels['cutypeindividual'] = 'Individual';
+$labels['cutypegroup'] = 'Grupo';
+$labels['cutyperesource'] = 'Recurso';
+$labels['cutyperoom'] = 'Sala';
+$labels['availfree'] = 'Disponível';
+$labels['availbusy'] = 'Ocupado';
+$labels['availunknown'] = 'Desconhecido';
+$labels['availtentative'] = 'Tentativa';
+$labels['availoutofoffice'] = 'Ausente';
+$labels['delegatedto'] = 'Delegado a:';
+$labels['delegatedfrom'] = 'Delegado de:';
+$labels['scheduletime'] = 'Procurar disponibilidade';
+$labels['sendinvitations'] = 'Enviar convites';
+$labels['sendnotifications'] = 'Avisar os participantes sobre as alterações';
+$labels['sendcancellation'] = 'Avisar os participantes sobre o cancelamento do evento';
+$labels['onlyworkinghours'] = 'Procurar disponibilidade dentro do meu horário de trabalho';
+$labels['reqallattendees'] = 'Necessário/todos os participantes';
+$labels['prevslot'] = 'Espaço anterior';
+$labels['nextslot'] = 'Próximo espaço';
+$labels['suggestedslot'] = 'Espaço sugerido';
+$labels['noslotfound'] = 'Incapaz de encontrar um horário disponível';
+$labels['invitationsubject'] = 'Foi convidado para "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nSegue em anexo um arquivo iCalendar com todos os detalhes de um evento, o qual pode importar para o seu calendário.";
+$labels['invitationattendlinks'] = "No caso do seu cliente de e-mail não suportar pedidos do tipo iTIP, pode usar o seguinte link para aceitar ou recusar este convite:\n\$url";
+$labels['eventupdatesubject'] = '"$title" foi atualizado.';
+$labels['eventupdatesubjectempty'] = 'Um evento do seu interesse foi atualizado';
+$labels['eventupdatemailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nSegue em anexo um arquivo iCalendar atualizado com os detalhes de um evento, o qual pode importar para o seu calendário.";
+$labels['eventcancelsubject'] = '"$title" foi cancelado.';
+$labels['eventcancelmailbody'] = "*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees\n\nO evento foi cancelado por \$organizer.\n\nSegue em anexo um arquivo iCalendar com os detalhes atualizados do evento.";
+$labels['itipobjectnotfound'] = 'O evento citado nesta mensagem não foi encontrado no seu calendário.';
+$labels['itipmailbodyaccepted'] = "\$sender aceitou o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender aceitou o convite como \"tentativa\" para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender recusou o convite para o seguinte evento:\n\n*\$title*\n\nQuando: \$date\n\nConvidados: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender rejeitou a sua participação no seguinte evento:\n\n*\$title*\n\nQuando: \$date";
+$labels['itipmailbodydelegated'] = "\$sender delegou a participação no seguinte evento:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender delegou a sua participação no seguinte evento:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipdeclineevent'] = 'Deseja recusar o convite para este evento?';
+$labels['declinedeleteconfirm'] = 'Também deseja apagar o evento recusado do seu calendário?';
+$labels['itipcomment'] = 'Observações — Convite/notificação';
+$labels['itipcommenttitle'] = 'Estas observações serão enviadas com o convite/notificação aos participantes.';
+$labels['notanattendee'] = 'Não está listado como participante neste evento';
+$labels['eventcancelled'] = 'O evento foi cancelado';
+$labels['saveincalendar'] = 'guardar em';
+$labels['updatemycopy'] = 'Atualizar no meu calendário';
+$labels['savetocalendar'] = 'Guardar no calendário';
+$labels['openpreview'] = 'Verificar calendário';
+$labels['noearlierevents'] = 'Sem eventos anteriores';
+$labels['nolaterevents'] = 'Sem eventos posteriores';
+$labels['resource'] = 'Recurso';
+$labels['addresource'] = 'Livro de recursos';
+$labels['findresources'] = 'Encontrar recursos';
+$labels['resourcedetails'] = 'Detalhes';
+$labels['resourceavailability'] = 'Disponibilidade';
+$labels['resourceowner'] = 'Dono';
+$labels['resourceadded'] = 'O recurso foi adicionado ao seu evento';
+$labels['tabsummary'] = 'Sumário';
+$labels['tabrecurrence'] = 'Repetição';
+$labels['tabattendees'] = 'Participantes';
+$labels['tabresources'] = 'Recursos';
+$labels['tabattachments'] = 'Anexos';
+$labels['tabsharing'] = 'Partilha';
+$labels['deleteobjectconfirm'] = 'Tem a certeza que quer eliminar este evento?';
+$labels['deleteventconfirm'] = 'Tem a certeza que quer eliminar este evento?';
+$labels['deletecalendarconfirm'] = 'Tem a certeza que quer eliminar este calendário e todos os seus eventos?';
+$labels['deletecalendarconfirmrecursive'] = 'Tem a certeza que quer eliminar este calendário com todos os seus eventos e sub-calendários?';
+$labels['savingdata'] = 'A guardar os dados...';
+$labels['errorsaving'] = 'Falha ao guardar as alterações.';
+$labels['operationfailed'] = 'A operação pedida falhou.';
+$labels['invalideventdates'] = 'As datas são inválidas! Por favor, verifique novamente.';
+$labels['invalidcalendarproperties'] = 'Propriedades de calendário inválidas! Por favor defina um nome válido.';
+$labels['searchnoresults'] = 'Nenhum evento encontrado nos calendários selecionados.';
+$labels['successremoval'] = 'O evento foi excluído com sucesso.';
+$labels['successrestore'] = 'O evento foi restaurado com sucesso.';
+$labels['errornotifying'] = 'Falha ao enviar notificações para os participantes do evento.';
+$labels['errorimportingevent'] = 'Falha ao importar evento';
+$labels['importwarningexists'] = 'Uma cópia deste evento já existe em seu calendário.';
+$labels['newerversionexists'] = 'Já existe uma nova versão deste evento! Abortado.';
+$labels['nowritecalendarfound'] = 'Nenhum calendário encontrado para salvar o evento';
+$labels['importedsuccessfully'] = 'O evento foi adicionado com sucesso em \'$calendar\'';
+$labels['updatedsuccessfully'] = 'O evento foi atualizado com sucesso em \'$calendar\'.';
+$labels['attendeupdateesuccess'] = 'O status do participante foi atualizado com sucesso.';
+$labels['itipsendsuccess'] = 'Convite enviado aos participantes.';
+$labels['itipresponseerror'] = 'Falha ao enviar a resposta para este convite de evento';
+$labels['itipinvalidrequest'] = 'Este convite já não é válido';
+$labels['sentresponseto'] = 'Resposta de convite enviada com sucesso para $mailto';
+$labels['localchangeswarning'] = 'As alterações que pretende efetuar só serão válidas no seu calendário e não serão enviadas ao organizador do evento.';
+$labels['importsuccess'] = 'Importado com sucesso $nr eventos';
+$labels['importnone'] = 'Não há eventos a serem importados';
+$labels['importerror'] = 'Ocorreu um erro na importação';
+$labels['aclnorights'] = 'Não tem permissão de administrador neste calendário.';
+$labels['changeeventconfirm'] = 'Alterar evento';
+$labels['removeeventconfirm'] = 'Eliminar evento';
+$labels['changerecurringeventwarning'] = 'Este evento é recorrente. Deseja alterar a ocorrência atual, esta e todas as futuras ocorrências ou guardar como um novo evento?';
+$labels['removerecurringeventwarning'] = 'Este evento é recorrente. Deseja eliminar a ocorrência atual, esta e todas as futuras ocorrências ou todas as ocorrências do evento?';
+$labels['removerecurringallonly'] = 'Este evento é recorrente. Como participante, só pode apagar apagar o evento com todas as ocorrências.';
+$labels['currentevent'] = 'Atual';
+$labels['futurevents'] = 'Futuro';
+$labels['allevents'] = 'Todos';
+$labels['saveasnew'] = 'Guardar como';
+$labels['birthdays'] = 'Aniversários';
+$labels['birthdayscalendar'] = 'Calendário de aniversários';
+$labels['displaybirthdayscalendar'] = 'Mostrar calendário de aniversários';
+$labels['birthdayscalendarsources'] = 'From these address books';
+$labels['birthdayeventtitle'] = 'Aniversário de $name';
+$labels['birthdayage'] = 'Idade $age';
+$labels['objectchangelog'] = 'Alterar histórico';
+$labels['revision'] = 'Revisão';
+$labels['user'] = 'Utilizador';
+$labels['operation'] = 'Ação';
+$labels['actionappend'] = 'Guardado';
+$labels['actionmove'] = 'Movido';
+$labels['actiondelete'] = 'Eliminado';
+$labels['compare'] = 'Comparar';
+$labels['showrevision'] = 'Mostrar esta versão';
+$labels['restore'] = 'Restaurar esta versão';
+$labels['objectnotfound'] = 'Falha ao ler os dados do evento';
+$labels['objectchangelognotavailable'] = 'Não é possível alterar o histórico deste evento';
+$labels['objectdiffnotavailable'] = 'Não é possível comparar as revisões selecionadas';
+$labels['revisionrestoreconfirm'] = 'Confirma o restauro da revisão $rev deste evento? Os dados atuais serão substituídos pelos da versão anterior.';
+$labels['arialabelminical'] = 'Seleção da data do calendário';
+$labels['arialabelcalendarview'] = 'Vista do calendário';
+$labels['arialabelsearchform'] = 'Quadro de pesquisa de eventos';
+$labels['arialabelquicksearchbox'] = 'Pesquisa de eventos';
+$labels['arialabelcalsearchform'] = 'Quadro de pesquisa de calendários';
+$labels['calendaractions'] = 'Ações do calendário';
+$labels['arialabeleventattendees'] = 'Lista de participantes do evento';
+$labels['arialabeleventresources'] = 'Lista de recursos para eventos';
+$labels['arialabelresourcesearchform'] = 'Quadro de pesquisa de recursos';
+$labels['arialabelresourceselection'] = 'Recursos disponíveis';
+?>
diff --git a/calendar/localization/ru_RU.inc b/calendar/localization/ru_RU.inc
new file mode 100644
index 0000000..b5c3c1d
--- /dev/null
+++ b/calendar/localization/ru_RU.inc
@@ -0,0 +1,277 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Вид по умолчанию';
+$labels['time_format'] = 'Формат времени';
+$labels['timeslots'] = 'Промежутков в час';
+$labels['first_day'] = 'Первый день недели';
+$labels['first_hour'] = 'Показывать начиная с';
+$labels['workinghours'] = 'Рабочие часы';
+$labels['add_category'] = 'Добавить категорию';
+$labels['remove_category'] = 'Удалить категорию';
+$labels['defaultcalendar'] = 'Создавать новое событие в';
+$labels['eventcoloring'] = 'Цвет события';
+$labels['coloringmode0'] = 'Согласно цвета календаря';
+$labels['coloringmode1'] = 'Согласно цвета категории';
+$labels['coloringmode2'] = 'Цвет календаря для рамки, цвет категории для фона';
+$labels['coloringmode3'] = 'Цвет категории для рамки, цвет календаря для фона';
+$labels['afternothing'] = 'Ничего не делать';
+$labels['aftertrash'] = 'Переместить в корзину';
+$labels['afterdelete'] = 'Удалить сообщение';
+$labels['afterflagdeleted'] = 'Пометить как удалённое';
+$labels['aftermoveto'] = 'Переместить в...';
+$labels['itipoptions'] = 'Приглашения на события';
+$labels['afteraction'] = 'После того, как приглашение или сообщение о его изменении обработано';
+$labels['calendar'] = 'Календарь';
+$labels['calendars'] = 'Календари';
+$labels['category'] = 'Категория';
+$labels['categories'] = 'Категории';
+$labels['createcalendar'] = 'Создать новый календарь';
+$labels['editcalendar'] = 'Редактировать свойства календаря';
+$labels['name'] = 'Имя';
+$labels['color'] = 'Цвет';
+$labels['day'] = 'День';
+$labels['week'] = 'Неделя';
+$labels['month'] = 'Месяц';
+$labels['agenda'] = 'Список';
+$labels['new'] = 'Новый';
+$labels['new_event'] = 'Новое событие';
+$labels['edit_event'] = 'Изменить событие';
+$labels['edit'] = 'Редактировать';
+$labels['save'] = 'Сохранить';
+$labels['removelist'] = 'Удалить из списка';
+$labels['cancel'] = 'Отмена';
+$labels['select'] = 'Выбрать';
+$labels['print'] = 'Распечатать';
+$labels['printtitle'] = 'Распечатать календарь';
+$labels['title'] = 'Сводка';
+$labels['description'] = 'Описание';
+$labels['all-day'] = 'весь день';
+$labels['export'] = 'Экспорт';
+$labels['exporttitle'] = 'Экспорт в iCalendar';
+$labels['exportrange'] = 'События начиная с';
+$labels['exportattachments'] = 'С вложениями';
+$labels['customdate'] = 'Специальная дата';
+$labels['location'] = 'Место';
+$labels['url'] = 'URL';
+$labels['date'] = 'Дата';
+$labels['start'] = 'Начало';
+$labels['starttime'] = 'Время начала';
+$labels['end'] = 'Конец';
+$labels['endtime'] = 'Время окончания';
+$labels['repeat'] = 'Повторить';
+$labels['selectdate'] = 'Выберите дату';
+$labels['freebusy'] = 'Показать как';
+$labels['free'] = 'Свободен';
+$labels['busy'] = 'Занят';
+$labels['outofoffice'] = 'Вне офиса';
+$labels['tentative'] = 'Неопределённо';
+$labels['mystatus'] = 'Мой статус';
+$labels['status'] = 'Статус';
+$labels['status-confirmed'] = 'Подтвеждённый';
+$labels['status-cancelled'] = 'Отмененные';
+$labels['priority'] = 'Приоритет';
+$labels['sensitivity'] = 'Секретность';
+$labels['public'] = 'общедоступная';
+$labels['private'] = 'личная';
+$labels['confidential'] = 'конфиденциальная';
+$labels['links'] = 'Ссылка';
+$labels['alarms'] = 'Напоминание';
+$labels['comment'] = 'Комментарий';
+$labels['created'] = 'Создана';
+$labels['changed'] = 'Изменена';
+$labels['unknown'] = 'Неизвестно';
+$labels['eventoptions'] = 'Опции';
+$labels['generated'] = 'создан';
+$labels['eventhistory'] = 'История';
+$labels['removelink'] = 'Удалить ссылку на письмо';
+$labels['printdescriptions'] = 'Печатать описания';
+$labels['parentcalendar'] = 'Вставить внутри';
+$labels['searchearlierdates'] = '« Искать события раньше';
+$labels['searchlaterdates'] = 'Искать события позже »';
+$labels['andnmore'] = '$nr больше...';
+$labels['togglerole'] = 'Кликните для переключения роли';
+$labels['createfrommail'] = 'Сохранить как событие';
+$labels['importevents'] = 'Импортировать события';
+$labels['importrange'] = 'События начиная с';
+$labels['onemonthback'] = '1 месяц назад';
+$labels['nmonthsback'] = '$nr месяца(ев) назад';
+$labels['showurl'] = 'Показать URL календаря';
+$labels['showurldescription'] = 'Используйте следующий адрес для просмотра Вашего календаря из других приложений. Вы можете скопировать и вставить это в любое приложение которое поддерживает формат iCal.';
+$labels['caldavurldescription'] = 'Скопируйте этот адрес в клиент, <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">поддерживающий CalDAV</a> (например, Evolution или Mozilla Thunderbird) для полной синхронизации данного календаря со своим компьютером или мобильным устройством.';
+$labels['findcalendars'] = 'Найти календари...';
+$labels['searchterms'] = 'Условия поиска';
+$labels['calsearchresults'] = 'Доступные календари';
+$labels['calendarsubscribe'] = 'Всегда показывать';
+$labels['nocalendarsfound'] = 'Календарей не найдено';
+$labels['nrcalendarsfound'] = '$nr календарей найдено';
+$labels['quickview'] = 'Просмотреть только этот календарь';
+$labels['invitationspending'] = 'Ожидающие приглашения';
+$labels['invitationsdeclined'] = 'Отклонённые приглашения';
+$labels['changepartstat'] = 'Изменить статус участника';
+$labels['rsvpcomment'] = 'Текст приглашения';
+$labels['listrange'] = 'Диапазон:';
+$labels['listsections'] = 'Разделить на:';
+$labels['smartsections'] = 'Умные секции';
+$labels['until'] = 'до';
+$labels['today'] = 'Сегодня';
+$labels['tomorrow'] = 'Завтра';
+$labels['thisweek'] = 'Текущая неделя';
+$labels['nextweek'] = 'Следующая неделя';
+$labels['prevweek'] = 'Предыдущая неделя';
+$labels['thismonth'] = 'Этот месяц';
+$labels['nextmonth'] = 'Следующий месяц';
+$labels['weekofyear'] = 'Неделя';
+$labels['pastevents'] = 'Прошедшее';
+$labels['futureevents'] = 'Будущее';
+$labels['showalarms'] = 'Показывать напоминания';
+$labels['defaultalarmtype'] = 'Настройки напоминания по умолчанию';
+$labels['defaultalarmoffset'] = 'Время напоминания по умолчанию';
+$labels['attendee'] = 'Участник';
+$labels['role'] = 'Роль';
+$labels['availability'] = 'Доступность';
+$labels['confirmstate'] = 'Статус';
+$labels['addattendee'] = 'Добавить участника';
+$labels['roleorganizer'] = 'Организатор';
+$labels['rolerequired'] = 'Обязательный';
+$labels['roleoptional'] = 'Необязательный';
+$labels['rolechair'] = 'Место';
+$labels['rolenonparticipant'] = 'Absent';
+$labels['cutypeindividual'] = 'Индивидуум';
+$labels['cutypegroup'] = 'Группа';
+$labels['cutyperesource'] = 'Ресурс';
+$labels['cutyperoom'] = 'Комната';
+$labels['availfree'] = 'Свободен';
+$labels['availbusy'] = 'Занят';
+$labels['availunknown'] = 'Неизвестно';
+$labels['availtentative'] = 'Предварительно';
+$labels['availoutofoffice'] = 'Вне офиса';
+$labels['delegatedto'] = 'Поручено:';
+$labels['delegatedfrom'] = 'Поручено от:';
+$labels['scheduletime'] = 'Найти доступность';
+$labels['sendinvitations'] = 'Отправить приглашения';
+$labels['sendnotifications'] = 'Уведомить участников об изменениях';
+$labels['sendcancellation'] = 'Уведомить участников об отмене события';
+$labels['onlyworkinghours'] = 'Найти доступность в мои рабочие часы';
+$labels['reqallattendees'] = 'Необходимые/все участники';
+$labels['prevslot'] = 'Предыдущее время';
+$labels['nextslot'] = 'Следующее время';
+$labels['suggestedslot'] = 'Предлагаемое время';
+$labels['noslotfound'] = 'Невозможно найти свободное время';
+$labels['invitationsubject'] = 'Вы приглашены на "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nКогда: \$date\n\nПриглашенные: \$attendees\n\nВо вложении вы найдёте файл iCalendar со всеми деталями события, который Вы можете импортировать в Вашу программу-ежедневник.";
+$labels['invitationattendlinks'] = "В случае, если Ваш почтовый клиент не поддерживает запросы iTip, Вы можете использовать ссылку данную ниже, чтобы принять или отклонить это приглашение:\n\$url";
+$labels['eventupdatesubject'] = '"$title" было обновлено';
+$labels['eventupdatesubjectempty'] = 'Событие, которое касается Вас, было обновлено';
+$labels['eventupdatemailbody'] = "*\$title*\n\nКогда: \$date\n\nПриглашенные: \$attendees\n\nВо вложении вы найдёте файл iCalendar со всеми изменениями в событии, который Вы можете импортировать в Вашу программу-ежедневник.";
+$labels['eventcancelsubject'] = '"$title" было отменено';
+$labels['eventcancelmailbody'] = "*\$title*\n\nКогда: \$date\n\nПриглашенные: \$attendees\n\nЭто событие отменено \$organizer.\n\nВо вложении вы найдёте файл iCalendar со всеми изменениями в событии.";
+$labels['itipobjectnotfound'] = 'Событие, упомянутое в этом сообщении, не найдено в вашем календаре.';
+$labels['itipmailbodyaccepted'] = "\$sender принял(а) приглашение на следующее событие:\n\n*\$title*\n\nКогда: \$date\n\nПриглашенные: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender предварительно принял(а) приглашение на следующее событие:\n\n*\$title*\n\nКогда: \$date\n\nПриглашенные: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender отклонил(а) приглашение на следующее событие:\n\n*\$title*\n\nКогда: \$date\n\nПриглашенные: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender отклонил ваше участие в событии:\n\n*\$title*\n\nДата: \$date";
+$labels['itipmailbodydelegated'] = "\$sender перепоручил(а) учестие в событии:\n\n*\$title*\n\nДата: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender перепоручил(а) Вам участие в событии:\n\n*\$title*\n\nДата: \$date";
+$labels['itipdeclineevent'] = 'Вы хотите отклонить приглашение на это событие?';
+$labels['declinedeleteconfirm'] = 'Хотите ли вы так же удалить это отклонённое событие из вашего календаря?';
+$labels['itipcomment'] = 'Комментарий к приглашению/извещению';
+$labels['itipcommenttitle'] = 'Этот комментарий будет прикреплён к приглашению/оповещению, отправленному участникам';
+$labels['notanattendee'] = 'Вы не в списке участников этого события';
+$labels['eventcancelled'] = 'Это событие отменено';
+$labels['saveincalendar'] = 'сохранить в';
+$labels['updatemycopy'] = 'Обновить в моём календаре';
+$labels['savetocalendar'] = 'Сохранить в календарь';
+$labels['openpreview'] = 'Проверить календарь';
+$labels['noearlierevents'] = 'Нет предыдущих событий';
+$labels['nolaterevents'] = 'Нет последующих событий';
+$labels['resource'] = 'Ресурс';
+$labels['addresource'] = 'Зарезервировать ресурс';
+$labels['findresources'] = 'Найти ресурсы';
+$labels['resourcedetails'] = 'Подробнее';
+$labels['resourceavailability'] = 'Доступность';
+$labels['resourceowner'] = 'Владелец';
+$labels['resourceadded'] = 'Ресурс добавлен в ваше событие';
+$labels['tabsummary'] = 'Сводка';
+$labels['tabrecurrence'] = 'Повторение';
+$labels['tabattendees'] = 'Участники';
+$labels['tabresources'] = 'Ресурсы';
+$labels['tabattachments'] = 'Вложения';
+$labels['tabsharing'] = 'Совместное использование';
+$labels['deleteobjectconfirm'] = 'Вы действительно хотите удалить это событие?';
+$labels['deleteventconfirm'] = 'Вы действительно хотите удалить это событие?';
+$labels['deletecalendarconfirm'] = 'Вы действительно хотите удалить этот календарь со всеми его событиями?';
+$labels['deletecalendarconfirmrecursive'] = 'Вы действительно хотите удалить этот календарь со всеми его событиями и вложенными календарями?';
+$labels['savingdata'] = 'Сохранение данных...';
+$labels['errorsaving'] = 'Ошибка сохранения изменений.';
+$labels['operationfailed'] = 'Не удалось выполнить запрошенную операцию.';
+$labels['invalideventdates'] = 'Неверная дата! Пожалуйста проверьте данные.';
+$labels['invalidcalendarproperties'] = 'Неверные свойства календаря! Пожалуйста введите допустимые данные.';
+$labels['searchnoresults'] = 'Событие не найдено в выбранных календарях.';
+$labels['successremoval'] = 'Событие успешно удалено.';
+$labels['successrestore'] = 'Событие успешно восстановлено.';
+$labels['errornotifying'] = 'Не удалось отправить уведомления участникам событий';
+$labels['errorimportingevent'] = 'Не удалось импортировать событие';
+$labels['importwarningexists'] = 'Копия этого события уже есть в вашем календаре.';
+$labels['newerversionexists'] = 'Обновлённая версия этого события уже существует! Отменено.';
+$labels['nowritecalendarfound'] = 'Не найден календарь для записи этого события';
+$labels['importedsuccessfully'] = 'Событие успешно добавлено в \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Событие успешно обновлено в \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Успешно обновлен статус участника';
+$labels['itipsendsuccess'] = 'Приглашания отправлены участникам.';
+$labels['itipresponseerror'] = 'Не удалось послать ответ на это приглашение';
+$labels['itipinvalidrequest'] = 'Это приглашение больше не действительно';
+$labels['sentresponseto'] = 'Успешно отправлен ответ на приглашение на $mailto';
+$labels['localchangeswarning'] = 'Вы собираетесь внести изменения, которые отразятся только на Вашем личном календаре и не будут отправлены организатору события.';
+$labels['importsuccess'] = 'Успешно импортировано $nr событий';
+$labels['importnone'] = 'Не найдено событий для импорта';
+$labels['importerror'] = 'Ошибка при импорте';
+$labels['aclnorights'] = 'Вы не имеете прав администратора для этого календаря.';
+$labels['changeeventconfirm'] = 'Изменить событие';
+$labels['removeeventconfirm'] = 'Удалить событие';
+$labels['changerecurringeventwarning'] = 'Это - повторяющееся событие. Хотели бы Вы редактировать только текущее событие, это и все будущие повторения, все события или сохранять его как новое событие?';
+$labels['removerecurringeventwarning'] = 'Это - повторяющееся событие. Хотели бы Вы удалить только текущее событие, это и все будущие события или все эти события?';
+$labels['removerecurringallonly'] = 'Это - повторяющееся событие. Как участник, Вы можете удалить всё событие вместе с всеми повторениями.';
+$labels['currentevent'] = 'Текущее';
+$labels['futurevents'] = 'Будущие';
+$labels['allevents'] = 'Все';
+$labels['saveasnew'] = 'Сохранить как новое';
+$labels['birthdays'] = 'Дни рождения';
+$labels['birthdayscalendar'] = 'Календарь Дней Рождения';
+$labels['displaybirthdayscalendar'] = 'Показывать календарь Дней Рождения';
+$labels['birthdayscalendarsources'] = 'Из этих адресных книг';
+$labels['birthdayeventtitle'] = 'День рождения $name';
+$labels['birthdayage'] = 'Возраст $age';
+$labels['objectchangelog'] = 'История изменений';
+$labels['objectdiff'] = 'Изменения с $rev1 до $rev2';
+$labels['revision'] = 'Ревизия';
+$labels['user'] = 'Пользователь';
+$labels['operation'] = 'Действие';
+$labels['actionappend'] = 'Сохранено';
+$labels['actionmove'] = 'Перемещено';
+$labels['actiondelete'] = 'Удалено';
+$labels['compare'] = 'Сравнить';
+$labels['showrevision'] = 'Показать эту версию';
+$labels['restore'] = 'Восстановить эту версию';
+$labels['objectnotfound'] = 'Не удалось загрузить информацию о мероприятиях';
+$labels['objectchangelognotavailable'] = 'История изменений для этого события недоступна';
+$labels['objectdiffnotavailable'] = 'Невозможно провести сравнение выбранных ревизий ';
+$labels['revisionrestoreconfirm'] = 'Вы уверенны, что хотите восстановить это событие из ревизии $rev? Оно заменит текущее событие старой версией. ';
+$labels['objectrestoresuccess'] = 'Ревизия $rev успешно восстановлена';
+$labels['objectrestoreerror'] = 'Не удалось восстановить старую ревизию';
+$labels['arialabelminical'] = 'Выбор даты';
+$labels['arialabelcalendarview'] = 'Вид календаря';
+$labels['arialabelsearchform'] = 'Форма поиска событий';
+$labels['arialabelquicksearchbox'] = 'Поиск событий';
+$labels['arialabelcalsearchform'] = 'Форма поиска календарей';
+$labels['calendaractions'] = 'Действия с календарями';
+$labels['arialabeleventattendees'] = 'Участники события';
+$labels['arialabeleventresources'] = 'Ресурсы события';
+$labels['arialabelresourcesearchform'] = 'Форма поиска ресурсов';
+$labels['arialabelresourceselection'] = 'Доступные ресурсы';
+?>
diff --git a/calendar/localization/sk_SK.inc b/calendar/localization/sk_SK.inc
new file mode 100644
index 0000000..44e9f15
--- /dev/null
+++ b/calendar/localization/sk_SK.inc
@@ -0,0 +1,82 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Prednastavené zobrazenie';
+$labels['time_format'] = 'Formát času';
+$labels['timeslots'] = 'Časových úsekov za hodinu';
+$labels['first_day'] = 'Prvý deň v týždni';
+$labels['first_hour'] = 'Prvá hodina na zobrazenie';
+$labels['workinghours'] = 'Pracovný čas';
+$labels['add_category'] = 'Pridať kategóriu';
+$labels['remove_category'] = 'Odstrániť kategóriu';
+$labels['defaultcalendar'] = 'Vytvoriť novú udalosť v';
+$labels['eventcoloring'] = 'Farba udalosti';
+$labels['afternothing'] = 'Neurobiť nič';
+$labels['aftertrash'] = 'Presunúť do koša';
+$labels['afterdelete'] = 'Vymazať správu';
+$labels['afterflagdeleted'] = 'Označiť ako vymazané';
+$labels['aftermoveto'] = 'Presunúť do...';
+$labels['itipoptions'] = 'Pozvánky na udalosť';
+$labels['calendar'] = 'Kalendár';
+$labels['calendars'] = 'Kalendáre';
+$labels['category'] = 'Kategória';
+$labels['categories'] = 'Kategórie';
+$labels['createcalendar'] = 'Vytvoriť nový kalendár';
+$labels['editcalendar'] = 'Upraviť nastavenie kalendára';
+$labels['name'] = 'Meno';
+$labels['color'] = 'Farba';
+$labels['day'] = 'Deň';
+$labels['week'] = 'Týždeň';
+$labels['month'] = 'Mesiac';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Nový';
+$labels['new_event'] = 'Nová udalosť';
+$labels['edit_event'] = 'Upraviť udalosť';
+$labels['edit'] = 'Upraviť';
+$labels['save'] = 'Uložiť';
+$labels['removelist'] = 'Odstrániť zo zoznamu';
+$labels['cancel'] = 'Zrušiť';
+$labels['select'] = 'Vybrať';
+$labels['print'] = 'Vytlačiť';
+$labels['printtitle'] = 'Vytlačiť kalendáre';
+$labels['title'] = 'Sumár';
+$labels['description'] = 'Popis';
+$labels['all-day'] = 'celý deň';
+$labels['export'] = 'Exportovať';
+$labels['exporttitle'] = 'Exportovať do iCalendar';
+$labels['exportrange'] = 'Udalosti z';
+$labels['exportattachments'] = 'S prílohami';
+$labels['customdate'] = 'Používateľský dátum';
+$labels['location'] = 'Poloha';
+$labels['url'] = 'URL';
+$labels['date'] = 'Dátum';
+$labels['start'] = 'Začiatok';
+$labels['starttime'] = 'Čas začiatku';
+$labels['end'] = 'Koniec';
+$labels['endtime'] = 'Čas konca';
+$labels['repeat'] = 'Opakovať';
+$labels['selectdate'] = 'Vyberte dátum';
+$labels['freebusy'] = 'Zobraziť ako';
+$labels['free'] = 'Voľný';
+$labels['busy'] = 'Zaneprázdnený';
+$labels['outofoffice'] = 'Mimo kancelárie';
+$labels['tentative'] = 'Nezáväzne';
+$labels['mystatus'] = 'Môj stav';
+$labels['status'] = 'Stav';
+$labels['status-confirmed'] = 'Potvrdené';
+$labels['status-cancelled'] = 'Zrušené';
+$labels['priority'] = 'Priorita';
+$labels['importrange'] = 'Udalosti z';
+$labels['weekofyear'] = 'Týždeň';
+$labels['confirmstate'] = 'Stav';
+$labels['availfree'] = 'Voľný';
+$labels['availbusy'] = 'Zaneprázdnený';
+$labels['availtentative'] = 'Nezáväzne';
+$labels['availoutofoffice'] = 'Mimo kancelárie';
+$labels['tabsummary'] = 'Sumár';
+?>
diff --git a/calendar/localization/sl_SI.inc b/calendar/localization/sl_SI.inc
new file mode 100644
index 0000000..0691d42
--- /dev/null
+++ b/calendar/localization/sl_SI.inc
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Privzeti pogled';
+$labels['time_format'] = 'Format časa';
+$labels['timeslots'] = 'Časovnih oken na uro';
+$labels['first_day'] = 'Prvi dan v tednu';
+$labels['first_hour'] = 'Prva ura za prikaz';
+$labels['workinghours'] = 'Ure dela';
+$labels['add_category'] = 'Dodaj kategorijo';
+$labels['remove_category'] = 'Odstrani kategorijo';
+$labels['defaultcalendar'] = 'Ustvari nove dogodke v';
+$labels['eventcoloring'] = 'Barva dogodka';
+$labels['coloringmode0'] = 'Po koledarju';
+$labels['coloringmode1'] = 'Po kategoriji';
+$labels['coloringmode2'] = 'Koledar za prikaz, kategorija za vsebino';
+$labels['coloringmode3'] = 'Kategorija za prikaz, koledar za vsebino';
+$labels['afternothing'] = 'Ne naredi ničesar';
+$labels['aftertrash'] = 'Premakni v koš';
+$labels['afterdelete'] = 'Izbriši sporočilo';
+$labels['afterflagdeleted'] = 'Označi kot izbrisano';
+$labels['aftermoveto'] = 'Premakni v...';
+$labels['itipoptions'] = 'Vabila za dogodek';
+$labels['afteraction'] = 'Po obdelavi vabila ali posodobljenega sporočila';
+$labels['calendar'] = 'Koledar';
+$labels['calendars'] = 'Koledarji';
+$labels['category'] = 'Kategorija';
+$labels['categories'] = 'Kategorije';
+$labels['createcalendar'] = 'Ustvari nov koledar';
+$labels['editcalendar'] = 'Uredi nastavitve koledarja';
+$labels['name'] = 'Ime';
+$labels['color'] = 'Barva';
+$labels['day'] = 'Dan';
+$labels['week'] = 'Teden';
+$labels['month'] = 'Mesec';
+$labels['agenda'] = 'Urnik';
+$labels['new'] = 'Nov';
+$labels['new_event'] = 'Nov dogodek';
+$labels['edit_event'] = 'Uredi dogodek';
+$labels['edit'] = 'Uredi';
+$labels['save'] = 'Shrani';
+$labels['removelist'] = 'Odstrani iz seznama';
+$labels['cancel'] = 'Prekliči';
+$labels['select'] = 'Izberi';
+$labels['print'] = 'Natisni';
+$labels['printtitle'] = 'Natisni koledarje';
+$labels['title'] = 'Pregled';
+$labels['description'] = 'Opis';
+$labels['all-day'] = 'cel dan';
+$labels['export'] = 'Izvozi';
+$labels['exporttitle'] = 'Izvozi v iCalendar';
+$labels['exportrange'] = 'Dogodki od';
+$labels['exportattachments'] = 'S priponkami';
+$labels['customdate'] = 'Poljubni datum';
+$labels['location'] = 'Lokacija';
+$labels['url'] = 'URL';
+$labels['date'] = 'Datum';
+$labels['start'] = 'Začetek';
+$labels['starttime'] = 'Čas začetka';
+$labels['end'] = 'Konec';
+$labels['endtime'] = 'Čas konca';
+$labels['repeat'] = 'Ponovi';
+$labels['selectdate'] = 'Izberi datum';
+$labels['freebusy'] = 'Prikaži me kot';
+$labels['free'] = 'Prost';
+$labels['busy'] = 'Zaseden';
+$labels['outofoffice'] = 'Izven pisarne';
+$labels['tentative'] = 'Pogojno';
+$labels['mystatus'] = 'Moj status';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Potrjeno';
+$labels['status-cancelled'] = 'Preklicano';
+$labels['priority'] = 'Prednost';
+$labels['sensitivity'] = 'Zasebnost';
+$labels['public'] = 'javno';
+$labels['private'] = 'zasebno';
+$labels['confidential'] = 'zaupno';
+$labels['links'] = 'Sklic';
+$labels['alarms'] = 'Opomnik';
+$labels['comment'] = 'Komentar';
+$labels['created'] = 'Ustvarjeno';
+$labels['changed'] = 'Nazadnje urejeno';
+$labels['unknown'] = 'Neznano';
+$labels['eventoptions'] = 'Nastavitve';
+$labels['generated'] = 'generirano ob';
+$labels['eventhistory'] = 'Zgodovina';
+$labels['removelink'] = 'Odstrani email povezavo';
+$labels['printdescriptions'] = 'Opis za tisk';
+$labels['parentcalendar'] = 'Vstavi';
+$labels['searchearlierdates'] = '« Išči po prejšnjih dogodkih';
+$labels['searchlaterdates'] = 'Išči po kasnejših dogodkih »';
+$labels['andnmore'] = '$nr več...';
+$labels['togglerole'] = 'Klikni za prikaz vloge';
+$labels['createfrommail'] = 'Shrani kot dogodek';
+$labels['importevents'] = 'Uvozi dogodke';
+$labels['importrange'] = 'Dogodki od';
+$labels['onemonthback'] = '1 mesec nazaj';
+$labels['nmonthsback'] = '$nr mesecev nazaj';
+$labels['showurl'] = 'Prikaži URL koledarja';
+$labels['showurldescription'] = 'Za dostop do koledarja (samo za branje) iz drugih aplikacij uporabi naslednji naslov. Funkcija kopiraj in prilepi deluje z vsakim koledarjem v iCal formatu.';
+$labels['caldavurldescription'] = 'Za sinhronizacijo tega koledarja z vašim računalnikom ali mobilno napravo, v podprto aplikacijo (npr. Evolution ali Mozilla Thunderbird) kopirajte ta naslov <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> .';
+$labels['findcalendars'] = 'Išči koledarje...';
+$labels['searchterms'] = 'Iskalni pogoji';
+$labels['calsearchresults'] = 'Razpoložljivi koledarji';
+$labels['calendarsubscribe'] = 'Označi za vedno';
+$labels['nocalendarsfound'] = 'Ni najdenih koledarjev';
+$labels['nrcalendarsfound'] = '$nr najdenih koledarjev';
+$labels['quickview'] = 'Prikaži samo ta koledar';
+$labels['invitationspending'] = 'Vabila za dogodek';
+$labels['invitationsdeclined'] = 'Zavrnjena vabila';
+$labels['changepartstat'] = 'Spremeni status sodelujočega';
+$labels['rsvpcomment'] = 'Sporočilo vabila';
+$labels['listrange'] = 'Prikaži v razponu:';
+$labels['listsections'] = 'Razdeli v:';
+$labels['smartsections'] = 'Pametni razdelki';
+$labels['until'] = 'do';
+$labels['today'] = 'Danes';
+$labels['tomorrow'] = 'Jutri';
+$labels['thisweek'] = 'Ta teden';
+$labels['nextweek'] = 'Naslednji teden';
+$labels['prevweek'] = 'Prejšnji teden';
+$labels['thismonth'] = 'Ta mesec';
+$labels['nextmonth'] = 'Naslednji mesec';
+$labels['weekofyear'] = 'Teden';
+$labels['pastevents'] = 'Pretekli';
+$labels['futureevents'] = 'Prihodnji';
+$labels['showalarms'] = 'Prikaži opomnike';
+$labels['defaultalarmtype'] = 'Privzeta nastavitev opomnika';
+$labels['defaultalarmoffset'] = 'Privzeti čas opomnika';
+$labels['attendee'] = 'Udeleženec';
+$labels['role'] = 'Vloga';
+$labels['availability'] = 'Razpol.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Dodaj udeleženca';
+$labels['roleorganizer'] = 'Organizator';
+$labels['rolerequired'] = 'Zahtevano';
+$labels['roleoptional'] = 'Neobvezno';
+$labels['rolechair'] = 'Vodja sestanka';
+$labels['rolenonparticipant'] = 'Odsoten';
+$labels['cutypeindividual'] = 'Osebni';
+$labels['cutypegroup'] = 'Skupina';
+$labels['cutyperesource'] = 'Vir';
+$labels['cutyperoom'] = 'Soba';
+$labels['availfree'] = 'Prost';
+$labels['availbusy'] = 'Zaseden';
+$labels['availunknown'] = 'Neznano';
+$labels['availtentative'] = 'Pogojno';
+$labels['availoutofoffice'] = 'Izven pisarne';
+$labels['delegatedto'] = 'Preneseno na:';
+$labels['delegatedfrom'] = 'Preneseno od:';
+$labels['scheduletime'] = 'Najdi razpoložljivost';
+$labels['sendinvitations'] = 'Pošlji vabila';
+$labels['sendnotifications'] = 'Sporoči udeležencem spremembe';
+$labels['sendcancellation'] = 'Sporoči udeležencem odpoved dogodka';
+$labels['onlyworkinghours'] = 'Najdi razpoložljivost med mojim delavnikom';
+$labels['reqallattendees'] = 'Zahtevano/vsi udeleženci';
+$labels['prevslot'] = 'Prejšnje mesto';
+$labels['nextslot'] = 'Naslednje mesto';
+$labels['suggestedslot'] = 'Predlagano mesto';
+$labels['noslotfound'] = 'Ne najdem prostega mesta';
+$labels['invitationsubject'] = 'Vabljeni ste v "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nProsim preglejte pripeto iCalendar datoteko z vsemi informacijami o dogodku. Datoteko lahko uvozite v vašo koledar aplikacijo.";
+$labels['invitationattendlinks'] = "V kolikor vaš email klient ne podpira iTip zahtevkov lahko uporabite naslednjo povezavo za sprejem ali zavrnitev vabila:\n\$url";
+$labels['eventupdatesubject'] = '"$title" je bil posodobljen';
+$labels['eventupdatesubjectempty'] = 'Dogodek, ki vas zadeva je bil posodobljen';
+$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nProsim preglejte pripeto iCalendar datoteko s posodobljenimi informacijami o dogodku. Datoteko lahko uvozite v vašo koledar aplikacijo.";
+$labels['eventcancelsubject'] = '"$title" je bil preklican';
+$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nDogodek je bil preklican s strani \$organizer.\n\nProsim preglejte pripeto iCalendar datoteko s posodobljenimi informacijami o dogodku.";
+$labels['itipobjectnotfound'] = 'Dogodek, na katerega se nanaša to sporočilo, ni bil najden v vašem koledarju.';
+$labels['itipmailbodyaccepted'] = "\$sender je sprejel vabilo na dogodek:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender je okvirno sprejel vabilo na dogodek:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender ni sprejel vabila na dogodek:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender je zavrnil vaše sodelovanje pri dogodku:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegated'] = "\$sender je prenesel sodelovanje na dogodku:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender je na vas prenesel sodelovanje na dogodku:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipdeclineevent'] = 'Želite zavrniti vabilo na ta dogodek?';
+$labels['declinedeleteconfirm'] = 'Ali želite tudi izbrisati zavrnjeni dogodek iz vašega koledarja?';
+$labels['itipcomment'] = 'Komentar vabila/obvestila';
+$labels['itipcommenttitle'] = 'Komentar bo dodan k vabilu/obvestilu, ki bo poslano sodelujočim';
+$labels['notanattendee'] = 'Niste označeni kot sodelujoči na tem sestanku';
+$labels['eventcancelled'] = 'Ta dogodek je bil preklican';
+$labels['saveincalendar'] = 'shrani v';
+$labels['updatemycopy'] = 'Posodobi v mojem koledarju';
+$labels['savetocalendar'] = 'Shrani v koledar';
+$labels['openpreview'] = 'Preveri Koledar';
+$labels['noearlierevents'] = 'Ni predhodnjih dogodkov';
+$labels['nolaterevents'] = 'Ni kasnejših dogodkov';
+$labels['resource'] = 'Vir';
+$labels['addresource'] = 'Označi vir';
+$labels['findresources'] = 'Poišči vire';
+$labels['resourcedetails'] = 'Podrobnosti';
+$labels['resourceavailability'] = 'Razpoložljivost';
+$labels['resourceowner'] = 'Lastnik';
+$labels['resourceadded'] = 'Vir je bil dodan v vašem dogodku';
+$labels['tabsummary'] = 'Pregled';
+$labels['tabrecurrence'] = 'Ponovitev';
+$labels['tabattendees'] = 'Sodelujoči';
+$labels['tabresources'] = 'Viri';
+$labels['tabattachments'] = 'Priponke';
+$labels['tabsharing'] = 'Deli z ostalimi';
+$labels['deleteobjectconfirm'] = 'Ali želite potrditi brisanje tega dogodka?';
+$labels['deleteventconfirm'] = 'Ali želite potrditi brisanje tega dogodka?';
+$labels['deletecalendarconfirm'] = 'Ali želite izbrisati ta koledar z vsemi dogodki?';
+$labels['deletecalendarconfirmrecursive'] = 'Ali želite izbrisati to koledar z vsemi dogodki in pod-koledarji?';
+$labels['savingdata'] = 'Shranjujem...';
+$labels['errorsaving'] = 'Napaka pri shranjevanju sprememb.';
+$labels['operationfailed'] = 'Zahtevana operacija ni uspela.';
+$labels['invalideventdates'] = 'Vnos datumov napačen! Prosim preverite vaš vnos.';
+$labels['invalidcalendarproperties'] = 'Napačne nastavitve koledarja! Prosim nastavite pravilno ime.';
+$labels['searchnoresults'] = 'V izbranih koledarjih ni dogodkov.';
+$labels['successremoval'] = 'Dogodek je bil uspešno izbrisan.';
+$labels['successrestore'] = 'Dogodek je bil uspešno obnovljen.';
+$labels['errornotifying'] = 'Napaka. Pošiljanje obvestil sodelujočim ni bilo uspešno.';
+$labels['errorimportingevent'] = 'Napaka pri uvozu dogodka';
+$labels['importwarningexists'] = 'Različica tega dogodka že obstaja v vašem koledarju.';
+$labels['newerversionexists'] = 'Obstaja novejša verzija tega dogodka!';
+$labels['nowritecalendarfound'] = 'Za shranjevanje dogodka ni na voljo nobenega koledarja';
+$labels['importedsuccessfully'] = 'Dogodek je bil uspešno dodan v \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Dogodek je bil uspešno posodobljen v \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Posodabljanje statusa sodelujočega uspešno';
+$labels['itipsendsuccess'] = 'Vabilo sodelujočim poslano';
+$labels['itipresponseerror'] = 'Napaka. Pošiljanje odgovora na vabilo ni bilo uspešno';
+$labels['itipinvalidrequest'] = 'To vabilo ni več veljavno';
+$labels['sentresponseto'] = 'Odgovor je bil uspešno poslan na naslov $mailto';
+$labels['localchangeswarning'] = 'Spremembe bodo vidne samo v vašem koledarju in ne bodo poslane organizatorju dogodka';
+$labels['importsuccess'] = 'Uspešno uvoženih $nr dogodkov';
+$labels['importnone'] = 'Ne najdem dogodkov za uvoz';
+$labels['importerror'] = 'Pri uvozu je prišlo do napake';
+$labels['aclnorights'] = 'Na tem koledarju nimate administratorskih pravic';
+$labels['changeeventconfirm'] = 'Spremeni dogodek';
+$labels['removeeventconfirm'] = 'Izbriši dogodek';
+$labels['changerecurringeventwarning'] = 'To je ponavljajoč dogodek. Ali želite urediti samo trenutni dogodek, trenutni in vse prihodnje dogodke, vse ponavljajoče dogodke ali shraniti kot nov dogodek?';
+$labels['removerecurringeventwarning'] = 'To je ponavljajoč dogodek. Ali želite izbrisati trenutni dogodek, trenutni in vse prihodnje dogodke ali vsa ponavljanja tega dogodka?';
+$labels['currentevent'] = 'Trenutni';
+$labels['futurevents'] = 'Prihodnji';
+$labels['allevents'] = 'Vsi';
+$labels['saveasnew'] = 'Shrani kot nov';
+$labels['birthdays'] = 'Rojstni dnevi';
+$labels['birthdayscalendar'] = 'Koledar rojstnih dnevov';
+$labels['displaybirthdayscalendar'] = 'Prikaži koledar rojstnih dnevov';
+$labels['birthdayscalendarsources'] = 'Iz teh imenikov';
+$labels['birthdayeventtitle'] = 'Rojstni dan osebe $name';
+$labels['birthdayage'] = 'Starost $age';
+$labels['objectchangelog'] = 'Spremeni Zgodovino';
+$labels['revision'] = 'Verzija';
+$labels['user'] = 'Uporabnik';
+$labels['operation'] = 'Dejanje';
+$labels['actionappend'] = 'Shranjeno';
+$labels['actionmove'] = 'Premaknjeno';
+$labels['actiondelete'] = 'Izbrisano';
+$labels['compare'] = 'Primerjaj';
+$labels['showrevision'] = 'Prikaži to verzijo';
+$labels['restore'] = 'Obnovi to verzijo';
+$labels['objectnotfound'] = 'Napaka pri nalaganju podatkov o dogodku';
+$labels['objectchangelognotavailable'] = 'Sprememba zgodovine za ta dogodek ni na voljo';
+$labels['objectdiffnotavailable'] = 'Primerjava za izbrane verzije ni na voljo';
+$labels['revisionrestoreconfirm'] = 'Ali želite obnoviti verzijo $rev tega dogodka? To bo nadomestilo trenutni dogodek s starejšo verzijo.';
+$labels['arialabelminical'] = 'Izbira datuma v koledarju';
+$labels['arialabelcalendarview'] = 'Prikaz koledarja';
+$labels['arialabelsearchform'] = 'Obrazec za iskanje dogodkov';
+$labels['arialabelquicksearchbox'] = 'Vnos iskanja dogodkov';
+$labels['arialabelcalsearchform'] = 'Obrazec za iskanje koledarjev';
+$labels['calendaractions'] = 'Dejanja koledarja';
+$labels['arialabeleventattendees'] = 'Seznam sodelujočih na dogodku';
+$labels['arialabeleventresources'] = 'Seznam virov za dogodek';
+$labels['arialabelresourcesearchform'] = 'Obrazec za iskanje virov';
+$labels['arialabelresourceselection'] = 'Viri na voljo';
+?>
diff --git a/calendar/localization/sv_SE.inc b/calendar/localization/sv_SE.inc
new file mode 100644
index 0000000..edf1db6
--- /dev/null
+++ b/calendar/localization/sv_SE.inc
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Standardvy';
+$labels['time_format'] = 'Tidformat';
+$labels['timeslots'] = 'Tidsluckor per timme';
+$labels['first_day'] = 'Första veckodag';
+$labels['first_hour'] = 'Dagen börjar';
+$labels['workinghours'] = 'Arbetstid';
+$labels['add_category'] = 'Lägg till kategori';
+$labels['remove_category'] = 'Ta bort kategori';
+$labels['defaultcalendar'] = 'Skapa nya händelser i';
+$labels['eventcoloring'] = 'Händelsefärg';
+$labels['coloringmode0'] = 'Enligt kalender';
+$labels['coloringmode1'] = 'Enligt kategori';
+$labels['coloringmode2'] = 'Kalender för översikt, kategori för innehåll';
+$labels['coloringmode3'] = 'Kategori för översikt, kalender för innehåll';
+$labels['afternothing'] = 'Gör inget';
+$labels['aftertrash'] = 'Flytta till papperskorgen';
+$labels['afterdelete'] = 'Ta bort meddelandet';
+$labels['afterflagdeleted'] = 'Märk som borttaget';
+$labels['aftermoveto'] = 'Flytta till ...';
+$labels['itipoptions'] = 'Händelseinbjudningar';
+$labels['afteraction'] = 'Efter en inbjudan eller uppdatering bearbetats meddelande';
+$labels['calendar'] = 'Kalender';
+$labels['calendars'] = 'Kalendrar';
+$labels['category'] = 'Kategori';
+$labels['categories'] = 'Kategorier';
+$labels['createcalendar'] = 'Skapa ny kalender';
+$labels['editcalendar'] = 'Redigera kalenderegenskaper';
+$labels['name'] = 'Namn';
+$labels['color'] = 'Färg';
+$labels['day'] = 'Dag';
+$labels['week'] = 'Vecka';
+$labels['month'] = 'Månad';
+$labels['agenda'] = 'Agenda';
+$labels['new'] = 'Ny';
+$labels['new_event'] = 'Ny händelse';
+$labels['edit_event'] = 'Redigera händelse';
+$labels['edit'] = 'Redigera';
+$labels['save'] = 'Spara';
+$labels['removelist'] = 'Ta bort från lista';
+$labels['cancel'] = 'Avbryt';
+$labels['select'] = 'Välj';
+$labels['print'] = 'Skriv ut';
+$labels['printtitle'] = 'Skriv ut kalendrar';
+$labels['title'] = 'Sammanfattning';
+$labels['description'] = 'Beskrivning';
+$labels['all-day'] = 'heldag';
+$labels['export'] = 'Exportera';
+$labels['exporttitle'] = 'Exportera till iCalendar';
+$labels['exportrange'] = 'Händelser från';
+$labels['exportattachments'] = 'Med bilagor';
+$labels['customdate'] = 'Anpassat datum';
+$labels['location'] = 'Plats';
+$labels['url'] = 'URL';
+$labels['date'] = 'Datum';
+$labels['start'] = 'Start';
+$labels['starttime'] = 'Starttid';
+$labels['end'] = 'Slut';
+$labels['endtime'] = 'Sluttid';
+$labels['repeat'] = 'Upprepa';
+$labels['selectdate'] = 'Välj datum';
+$labels['freebusy'] = 'Visa mig som';
+$labels['free'] = 'Ledig';
+$labels['busy'] = 'Upptagen';
+$labels['outofoffice'] = 'Frånvarande';
+$labels['tentative'] = 'Preliminärt';
+$labels['mystatus'] = 'Min status';
+$labels['status'] = 'Status';
+$labels['status-confirmed'] = 'Bekräftad';
+$labels['status-cancelled'] = 'Inställd';
+$labels['priority'] = 'Prioritet';
+$labels['sensitivity'] = 'Integritet';
+$labels['public'] = 'publik';
+$labels['private'] = 'privat';
+$labels['confidential'] = 'konfidentiell';
+$labels['links'] = 'Referens';
+$labels['alarms'] = 'Påminnelse';
+$labels['comment'] = 'Kommentar';
+$labels['created'] = 'Skapad';
+$labels['changed'] = 'Senast ändrad';
+$labels['unknown'] = 'Okänd';
+$labels['eventoptions'] = 'Alternativ';
+$labels['generated'] = 'genererad vid';
+$labels['eventhistory'] = 'Historik';
+$labels['removelink'] = 'Ta bort e-postreferens';
+$labels['printdescriptions'] = 'Skriv ut beskrivning';
+$labels['parentcalendar'] = 'Infoga inuti';
+$labels['searchearlierdates'] = '« Sök efter tidigare händelser';
+$labels['searchlaterdates'] = 'Sök efter senare händelser »';
+$labels['andnmore'] = '$nr fler ...';
+$labels['togglerole'] = 'Klicka för att växla roll';
+$labels['createfrommail'] = 'Spara som händelse';
+$labels['importevents'] = 'Importera händelser';
+$labels['importrange'] = 'Händelser från';
+$labels['onemonthback'] = '1 månad bakåt';
+$labels['nmonthsback'] = '$nr månader bakåt';
+$labels['showurl'] = 'Visa kalender-URL';
+$labels['showurldescription'] = 'Använd följande adress för åtkomst (endast läsbar) till din kalender från andra applikationer. Du kan kopiera och klistra in adressen in i ett kalenderprogram som stöder iCal-formatet.';
+$labels['caldavurldescription'] = 'Kopiera denna adress till ett <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> klientprogram (t.ex. Evolution eller Mozilla Thunderbird) för att helt synkronisera denna specifika kalender med din dator eller mobila enhet.';
+$labels['findcalendars'] = 'Hitta kalendrar ...';
+$labels['searchterms'] = 'Sökord';
+$labels['calsearchresults'] = 'Tillgängliga kalendrar';
+$labels['calendarsubscribe'] = 'Lista permanent';
+$labels['nocalendarsfound'] = 'Inga kalendrar hittades';
+$labels['nrcalendarsfound'] = '$nr kalendrar hittades';
+$labels['quickview'] = 'Visa endast denna kalender';
+$labels['invitationspending'] = 'Avvaktade inbjudningar';
+$labels['invitationsdeclined'] = 'Avböjda inbjudningar';
+$labels['changepartstat'] = 'Ändra deltagarstatus';
+$labels['rsvpcomment'] = 'Inbjudningstext';
+$labels['listrange'] = 'Intervall att visa';
+$labels['listsections'] = 'Dela upp i';
+$labels['smartsections'] = 'Smarta sektioner';
+$labels['until'] = 'tills';
+$labels['today'] = 'Idag';
+$labels['tomorrow'] = 'Imorgon';
+$labels['thisweek'] = 'Denna vecka';
+$labels['nextweek'] = 'Nästa vecka';
+$labels['prevweek'] = 'Föregående vecka';
+$labels['thismonth'] = 'Denna månad';
+$labels['nextmonth'] = 'Nästa månad';
+$labels['weekofyear'] = 'Vecka';
+$labels['pastevents'] = 'Förflutna';
+$labels['futureevents'] = 'Framtida';
+$labels['showalarms'] = 'Visa påminnelser';
+$labels['defaultalarmtype'] = 'Standardinställning för påminnelser';
+$labels['defaultalarmoffset'] = 'Standartid för påminnelser';
+$labels['attendee'] = 'Deltagare';
+$labels['role'] = 'Roll';
+$labels['availability'] = 'Tillg.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Lägg till deltagare';
+$labels['roleorganizer'] = 'Organisatör';
+$labels['rolerequired'] = 'Obligatorisk';
+$labels['roleoptional'] = 'Valfritt';
+$labels['rolechair'] = 'Stol';
+$labels['rolenonparticipant'] = 'Frånvarande';
+$labels['cutypeindividual'] = 'Individuell';
+$labels['cutypegroup'] = 'Grupp';
+$labels['cutyperesource'] = 'Resurs';
+$labels['cutyperoom'] = 'Rum';
+$labels['availfree'] = 'Ledig';
+$labels['availbusy'] = 'Upptagen';
+$labels['availunknown'] = 'Okänd';
+$labels['availtentative'] = 'Preliminärt';
+$labels['availoutofoffice'] = 'Frånvarande';
+$labels['delegatedto'] = 'Delegerad till:';
+$labels['delegatedfrom'] = 'Delegerad från:';
+$labels['scheduletime'] = 'Hitta tillgänglighet';
+$labels['sendinvitations'] = 'Skicka inbjudningar';
+$labels['sendnotifications'] = 'Meddela deltagare om ändringar';
+$labels['sendcancellation'] = 'Meddela deltagare om att händelsen ställts in';
+$labels['onlyworkinghours'] = 'Sök tillgänglighet inom min arbetstid';
+$labels['reqallattendees'] = 'Obligatorisk/alla deltagare';
+$labels['prevslot'] = 'Föregående lucka';
+$labels['nextslot'] = 'Nästa lucka';
+$labels['suggestedslot'] = 'Föreslagen lucka';
+$labels['noslotfound'] = 'Det gick inte att hitta en ledig tidslucka';
+$labels['invitationsubject'] = 'Du har blivit inbjuden till "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nNär: \$date\n\nInbjudna: \$attendees\n\nHärmed bifogas en iCalendar-fil med alla detaljer om händelsen som du kan importera till din kalenderapplikation.";
+$labels['invitationattendlinks'] = "Om din e-postklient inte stöder iTip-förfrågningar kan du använda följande länk för att antingen tacka ja eller eller nej till denna inbjudan:\n\$url";
+$labels['eventupdatesubject'] = '"$title" har uppdaterats';
+$labels['eventupdatesubjectempty'] = 'En händelse som berör dig har uppdaterats';
+$labels['eventupdatemailbody'] = "*\$title*\n\nNär: \$date\n\nInbjudna: \$attendees\n\nHärmed bifogas en iCalendar-fil med uppdaterad information som du kan importera till din kalenderapplikation.";
+$labels['eventcancelsubject'] = '"$title" har ställts in';
+$labels['eventcancelmailbody'] = "*\$title*\n\nNär: \$date\n\nInbjudna: \$attendees\n\nHändelsen har ställts in av \$organizer.\n\nHärmed bifogas en iCalendar-fil med uppdaterad information om händelsen.";
+$labels['itipobjectnotfound'] = 'Den händelse som avses i detta meddelande hittades inte i din kalender.';
+$labels['itipmailbodyaccepted'] = "\$sender har accepterat inbjudan till följande händelse:\n\n*\$title*\n\nNär: \$date\n\nInbjudna: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender har preliminärt accepterat inbjudan till följande evenemang:\n\n*\$title*\n\nNär: \$date\n\nInbjudna: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender har tackat nej till inbjudan till följande händelse:\n\n*\$title*\n\nNär: \$date\n\nIbjudna: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender har avvisat ditt deltagande i följande händelse:\n\n*\$title*\n\nNär: \$dat";
+$labels['itipmailbodydelegated'] = "\$sender har delegerat deltagandet i följande händelse:\n\n*\$title*\n\nNär: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender har delegerat deltagandet i följande händelse till dig:\n\n*\$title*\n\nNär: \$date";
+$labels['itipdeclineevent'] = 'Vill du tacka nej till inbjudan till denna händelse?';
+$labels['declinedeleteconfirm'] = 'Vill du även ta bort denna händelse, som du tackat nej till, från din kalender?';
+$labels['itipcomment'] = 'Kommentar till inbjudan/meddelande';
+$labels['itipcommenttitle'] = 'Denna kommentar kommer att bifogas i inbjudan/meddelandet som skickas till deltagarna';
+$labels['notanattendee'] = 'Du är inte listad som en deltagare i denna händelse';
+$labels['eventcancelled'] = 'Händelsen har ställts in';
+$labels['saveincalendar'] = 'Spara i';
+$labels['updatemycopy'] = 'Uppdatera i min kalender';
+$labels['savetocalendar'] = 'Spara till kalender';
+$labels['openpreview'] = 'Kontrollera kalender';
+$labels['noearlierevents'] = 'Inga tidigare händelser';
+$labels['nolaterevents'] = 'Inga senare händelser';
+$labels['resource'] = 'Resurs';
+$labels['addresource'] = 'Boka resurs';
+$labels['findresources'] = 'Hitta resurser';
+$labels['resourcedetails'] = 'Detaljer';
+$labels['resourceavailability'] = 'Tillgänglighet';
+$labels['resourceowner'] = 'Ägare';
+$labels['resourceadded'] = 'Resursen har kopplats till din händelser ';
+$labels['tabsummary'] = 'Sammanfattning';
+$labels['tabrecurrence'] = 'Återkommande';
+$labels['tabattendees'] = 'Deltagare';
+$labels['tabresources'] = 'Resurser';
+$labels['tabattachments'] = 'Bilagor';
+$labels['tabsharing'] = 'Delning';
+$labels['deleteobjectconfirm'] = 'Vill du verkligen ta bort denna händelse';
+$labels['deleteventconfirm'] = 'Vill du verkligen ta bort denna händelse';
+$labels['deletecalendarconfirm'] = 'Vill du verkligen ta bort denna kalender med alla dess händelser?';
+$labels['deletecalendarconfirmrecursive'] = 'Vill du verkligen ta bort denna kalender med alla händelser och delkalendrar?';
+$labels['savingdata'] = 'Sparar data ...';
+$labels['errorsaving'] = 'Misslyckades att spara ändringar.';
+$labels['operationfailed'] = 'Den begärda åtgärden misslyckades.';
+$labels['invalideventdates'] = 'Ogiltiga datum angivna! Kontrollera din inmatning.';
+$labels['invalidcalendarproperties'] = 'Ogiltiga kalenderegenskaper! Var god ange ett giltigt namn.';
+$labels['searchnoresults'] = 'Inga händelser hittades i de valda kalendrarna.';
+$labels['successremoval'] = 'Händelsen har tagits bort.';
+$labels['successrestore'] = 'Händelsen har återställts.';
+$labels['errornotifying'] = 'Det gick inte att skicka meddelande till händelsens deltagare';
+$labels['errorimportingevent'] = 'Det gick inte att importera händelsen';
+$labels['importwarningexists'] = 'En kopia av denna händelse finns redan i din kalender.';
+$labels['newerversionexists'] = 'En nyare version av denna händelse finns redan! Åtgärd avbruten.';
+$labels['nowritecalendarfound'] = 'Ingen kalender hittades att spara händelsen i';
+$labels['importedsuccessfully'] = 'Händelsen har lagts till i \'$calendar\'';
+$labels['updatedsuccessfully'] = 'Händelsen uppdaterades i \'$calendar\'';
+$labels['attendeupdateesuccess'] = 'Uppdaterade deltagarens status';
+$labels['itipsendsuccess'] = 'Inbjudan har skickats till deltagare.';
+$labels['itipresponseerror'] = 'Det gick inte att skicka ett svar på inbjudan till denna händelse';
+$labels['itipinvalidrequest'] = 'Denna inbjudan är inte längre giltig';
+$labels['sentresponseto'] = 'Skickade svar på inbjudan till $mailto';
+$labels['localchangeswarning'] = 'Du är på väg att göra ändringar som endast kommer att återspeglas i din kalender och inte skickas till organisatören av händelsen.';
+$labels['importsuccess'] = 'Importerade $nr händelser';
+$labels['importnone'] = 'Hittade inga händelser att importera';
+$labels['importerror'] = 'Ett fel uppstod vid import';
+$labels['aclnorights'] = 'Du har inga administratörsrättigheter i denna kalender.';
+$labels['changeeventconfirm'] = 'Ändra händelse';
+$labels['removeeventconfirm'] = 'Ta bort händelse';
+$labels['changerecurringeventwarning'] = 'Detta är en återkommande händelse. Vill du redigera endast den aktuella händelsen, denna och alla framtida händelser, alla händelser eller spara den som en ny händelse?';
+$labels['removerecurringeventwarning'] = 'etta är en återkommande händelse. Vill du ta bort endast den aktuella händelsen, denna och alla framtida händelser eller alla förekomster av denna händelse';
+$labels['currentevent'] = 'Nuvarande';
+$labels['futurevents'] = 'Framtida';
+$labels['allevents'] = 'Alla';
+$labels['saveasnew'] = 'Spara som ny';
+$labels['birthdays'] = 'Födelsedagar';
+$labels['birthdayscalendar'] = 'Födelsedagskalender';
+$labels['displaybirthdayscalendar'] = 'Visa födelsedagskalender';
+$labels['birthdayscalendarsources'] = 'Från dessa adressböcker';
+$labels['birthdayeventtitle'] = '$name\s födelsedag';
+$labels['birthdayage'] = '$age år';
+$labels['objectchangelog'] = 'Ändringshistorik';
+$labels['revision'] = 'Revision';
+$labels['user'] = 'Användare';
+$labels['operation'] = 'Åtgärd';
+$labels['actionappend'] = 'Sparad';
+$labels['actionmove'] = 'Flyttad';
+$labels['actiondelete'] = 'Borttagen';
+$labels['compare'] = 'Jämför';
+$labels['showrevision'] = 'Visa denna version';
+$labels['restore'] = 'Återställ denna verson';
+$labels['objectnotfound'] = 'Det gick inte att läsa in data för händelsen';
+$labels['objectchangelognotavailable'] = 'Ändringshistorik är inte tillgänglig för denna händelse';
+$labels['objectdiffnotavailable'] = 'Ingen jämförelse möjlig för valda revisioner';
+$labels['revisionrestoreconfirm'] = 'Vill du verkligen återställa revision $rev för denna händelse? Det kommer att ersätta den aktuella händelsen med den gamla versionen.';
+$labels['arialabelminical'] = 'Kalender datumurval';
+$labels['arialabelcalendarview'] = 'Kalender vy';
+$labels['arialabelsearchform'] = 'Händelse sökformulär';
+$labels['arialabelquicksearchbox'] = 'Händelse sökinmatning';
+$labels['arialabelcalsearchform'] = 'Kalender sökformulär';
+$labels['calendaractions'] = 'Kalender åtgärder';
+$labels['arialabeleventattendees'] = 'Händelse deltagarlista';
+$labels['arialabeleventresources'] = 'Händelse resurslista';
+$labels['arialabelresourcesearchform'] = 'Resurser sökformulär';
+$labels['arialabelresourceselection'] = 'Tillgängliga resurser';
+?>
diff --git a/calendar/localization/th_TH.inc b/calendar/localization/th_TH.inc
new file mode 100644
index 0000000..c9933fc
--- /dev/null
+++ b/calendar/localization/th_TH.inc
@@ -0,0 +1,257 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'มุมมองเริ่มต้น';
+$labels['time_format'] = 'รูปแบบของการแสดงเวลา';
+$labels['timeslots'] = 'จำนวนช่องเวลาต่อชั่วโมง';
+$labels['first_day'] = 'วันแรกของสัปดาห์';
+$labels['first_hour'] = 'ชั่วโมงแรกที่เริ่มแสดงผล';
+$labels['workinghours'] = 'ชั่วโมงทำงาน';
+$labels['add_category'] = 'เพิ่มหมวดหมู่';
+$labels['remove_category'] = 'ลบหมวดหมู่';
+$labels['defaultcalendar'] = 'เพิ่มนัดหมายใหม่ใน';
+$labels['eventcoloring'] = 'การให้สีเหตุการณ์ต่างๆ';
+$labels['coloringmode0'] = 'ตามปฎิทิน';
+$labels['coloringmode1'] = 'ตามหมวดหมู่';
+$labels['afternothing'] = 'ไม่ต้องทำอะไร';
+$labels['aftertrash'] = 'ย้ายลงถังขยะ';
+$labels['afterdelete'] = 'ลบข้อความ';
+$labels['afterflagdeleted'] = 'ติดธงว่าลบแล้ว';
+$labels['aftermoveto'] = 'ย้ายไปยัง...';
+$labels['itipoptions'] = 'เชิญเข้าร่วมนัดหมาย';
+$labels['afteraction'] = 'ภายหลังการประมวลผลข้อความ คำเชิญ หรือ ปรับปรุงสถานภาพ';
+$labels['calendar'] = 'ปฎิทิน';
+$labels['calendars'] = 'ปฎิทิน';
+$labels['category'] = 'หมวดหมู่';
+$labels['categories'] = 'หมวดหมู่';
+$labels['createcalendar'] = 'สร้างปฎิทินฉบับใหม่';
+$labels['editcalendar'] = 'แก้ไขคุณสมบัติของปฎิทิน';
+$labels['name'] = 'ชื่อ';
+$labels['color'] = 'สี';
+$labels['day'] = 'วัน';
+$labels['week'] = 'สัปดาห์';
+$labels['month'] = 'เดือน';
+$labels['new'] = 'เพิ่ม';
+$labels['new_event'] = 'เพิ่มนัดหมาย';
+$labels['edit_event'] = 'แก้ไขนัดหมาย';
+$labels['edit'] = 'แก้ไข';
+$labels['save'] = 'บันทึก';
+$labels['removelist'] = 'นำออกจากรายการ';
+$labels['cancel'] = 'ยกเลิก';
+$labels['select'] = 'เลือก';
+$labels['print'] = 'พิมพ์';
+$labels['printtitle'] = 'พิมพ์ปฎิทิน';
+$labels['title'] = 'สรุป';
+$labels['description'] = 'คำอธิบาย';
+$labels['all-day'] = 'ทั้งวัน';
+$labels['export'] = 'ส่งออก';
+$labels['exporttitle'] = 'ส่งออกไปยัง iCalendar';
+$labels['exportrange'] = 'เหตุการณ์จาก';
+$labels['exportattachments'] = 'พร้อมสิ่งที่แนบมาด้วย';
+$labels['location'] = 'สถานที่';
+$labels['date'] = 'วันที่';
+$labels['start'] = 'เริ่ม';
+$labels['starttime'] = 'เวลาเริ่ม';
+$labels['end'] = 'จบ';
+$labels['endtime'] = 'กำหนดเสร็จ';
+$labels['repeat'] = 'เกิดซ้ำ';
+$labels['selectdate'] = 'เลือกวันที่';
+$labels['free'] = 'ว่าง';
+$labels['busy'] = 'ติดธุระ';
+$labels['outofoffice'] = 'อยู่นอกออฟฟิศ';
+$labels['tentative'] = 'แนวโน้ม';
+$labels['mystatus'] = 'สถานะของฉัน';
+$labels['status'] = 'สถานะ';
+$labels['status-confirmed'] = 'ยืนยัน';
+$labels['status-cancelled'] = 'ยกเลิก';
+$labels['priority'] = 'ความสำคัญ';
+$labels['sensitivity'] = 'ความเป็นส่วนตัว';
+$labels['public'] = 'สาธารณะ';
+$labels['private'] = 'ส่วนตัว';
+$labels['confidential'] = 'ลับเฉพาะ';
+$labels['links'] = 'อ้างอิง';
+$labels['alarms'] = 'คำแจ้งเตือน';
+$labels['comment'] = 'ความคิดเห็น';
+$labels['created'] = 'สร้างเมื่อ';
+$labels['changed'] = 'แก้ไขครั้งสุดท้ายเมื่อ';
+$labels['unknown'] = 'ไม่ทราบ';
+$labels['eventoptions'] = 'ทางเลือก';
+$labels['eventhistory'] = 'ประวัติ';
+$labels['printdescriptions'] = 'พิมพ์คำอธิบาย';
+$labels['parentcalendar'] = 'เพิ่มเข้าภายใต้';
+$labels['searchearlierdates'] = '« ค้นหากำหนดการณ์ที่เกิดชึ้นก่อนหน้า';
+$labels['searchlaterdates'] = 'ค้นหากำหนดการณ์ที่เกิดขึ้นหลังจาก »';
+$labels['andnmore'] = 'มีอีก $nr';
+$labels['togglerole'] = 'กดเพื่อเปลี่ยนสลับบทบาท';
+$labels['createfrommail'] = 'บันทึกเป็นเหตุการณ์';
+$labels['importevents'] = 'นำเข้าเหตุการณ์';
+$labels['importrange'] = 'เหตุการณ์จาก';
+$labels['onemonthback'] = 'ย้อนหลัง 1 เดือน';
+$labels['nmonthsback'] = 'ย้อนหลัง $nr เดือน';
+$labels['showurl'] = 'แสดงลิงค์ปฎิทิน';
+$labels['showurldescription'] = 'ใช้ลิงค์ที่อยู่ต่อไปนี้เพื่อเข้าถึง (อ่านเท่านั้น) ปฎิทินของคุณจากโปรแกรมอื่น คุณสามารถคัดลอกและนำไปวางไว้ในซอฟท์แวร์ปฎิทินที่รับรองรูปแบบ iCal';
+$labels['caldavurldescription'] = 'คัดลอกลิงค์ที่อยู่นี้ไปยัง <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> โปรแกรมในเครื่องลูกข่าย (เช่น Evolution หรือ Mozilla Thunderbird) เพื่อซิงค์ข้อมูลปฎิทินฉบับนี้กับคอมพิวเตอร์หรืออุปกรณ์มือถือของคุณ';
+$labels['findcalendars'] = 'ค้นหาปฎิทิน...';
+$labels['searchterms'] = 'ข้อความที่ต้องการค้นหา';
+$labels['calsearchresults'] = 'ปฎิทินที่มีให้เลือก';
+$labels['calendarsubscribe'] = 'แสดงเป็นรายการถาวร';
+$labels['nocalendarsfound'] = 'ไม่พบปฎิทิน';
+$labels['nrcalendarsfound'] = 'พบปฎิทิน $nr ฉบับ';
+$labels['quickview'] = 'ดูเฉพาะปฎิทินฉบับนี้';
+$labels['invitationspending'] = 'จดหมายเชิญที่ยังค้างอยู่';
+$labels['invitationsdeclined'] = 'ปฎิเสธคำเชิญ';
+$labels['changepartstat'] = 'เปลี่ยนสถานะของผู้เข้าร่วม';
+$labels['rsvpcomment'] = 'คำเชิญ';
+$labels['listrange'] = 'ขอบเขตุที่แสดง';
+$labels['listsections'] = 'แบ่งเป็น:';
+$labels['until'] = 'จนถึง';
+$labels['today'] = 'วันนี้';
+$labels['tomorrow'] = 'พรุ่งนี้';
+$labels['thisweek'] = 'สัปดาห์นี้';
+$labels['nextweek'] = 'สัปดาห์หน้า';
+$labels['prevweek'] = 'สัปดาห์ที่แล้ว';
+$labels['thismonth'] = 'เดือนนี้';
+$labels['nextmonth'] = 'เดือนหน้า';
+$labels['weekofyear'] = 'สัปดาห์';
+$labels['pastevents'] = 'อดีต';
+$labels['futureevents'] = 'อนาคต';
+$labels['showalarms'] = 'แสดงข้อความเตือน';
+$labels['defaultalarmtype'] = 'ค่าการแจ้งเตือนเริ่มต้น';
+$labels['attendee'] = 'ผู้เข้าร่วม';
+$labels['role'] = 'บทบาท';
+$labels['confirmstate'] = 'สถานะ';
+$labels['addattendee'] = 'เพิ่มผู้เข้าร่วม';
+$labels['roleorganizer'] = 'ผู้จัดงาน';
+$labels['rolerequired'] = 'บังคับ';
+$labels['roleoptional'] = 'เลือกได้';
+$labels['rolenonparticipant'] = 'ขาด';
+$labels['cutypeindividual'] = 'บุคคล';
+$labels['cutypegroup'] = 'กลุ่ม';
+$labels['cutyperesource'] = 'ทรัพยากร';
+$labels['cutyperoom'] = 'ห้อง';
+$labels['availfree'] = 'ว่าง';
+$labels['availbusy'] = 'ติดธุระ';
+$labels['availunknown'] = 'ไม่ทราบ';
+$labels['availtentative'] = 'แนวโน้ม';
+$labels['availoutofoffice'] = 'ไม่อยู่ออฟฟิศ';
+$labels['delegatedto'] = 'มอบหมายให้';
+$labels['delegatedfrom'] = 'รับมอบจาก';
+$labels['scheduletime'] = 'ค้นหาส่วนที่ว่าง';
+$labels['sendinvitations'] = 'ส่งคำเชิญ';
+$labels['sendnotifications'] = 'แจ้งเตือนผู้เข้าร่วมสำหรับการแก้ไข';
+$labels['sendcancellation'] = 'แจ้งเตือนผู้เข้าร่วมเกี่ยวกับการยกเลิก';
+$labels['onlyworkinghours'] = 'ค้นหาเวลาว่างในช่วงชั่วโมงการทำงานของฉัน';
+$labels['reqallattendees'] = 'บังคับ/ผู้เข้าร่วมทุกคน';
+$labels['prevslot'] = 'ช่องว่างก่อนหน้านี้';
+$labels['nextslot'] = 'ช่องว่า่งถัดจากนี้';
+$labels['suggestedslot'] = 'แนะนำช่องว่าง';
+$labels['noslotfound'] = 'ไม่สามารถหาช่วงเวลาที่ว่าง';
+$labels['invitationsubject'] = 'คุณได้รับเชิญไปยัง "$title"';
+$labels['invitationattendlinks'] = "ในกรณีที่โปรแกรมอีเมล์ของคุณไม่รองรับ 'การร้องขอ iTip' คุณสามารถใช้ลิงค์ต่อไปนี้ในการตอบรับหรือปฎิเสธจดหมายเชิญฉบับนี้ :\n\$url";
+$labels['eventupdatesubject'] = '"$title" ได้รับการปรับปรุงสถานะ';
+$labels['eventupdatesubjectempty'] = 'เหตุการณ์ที่คุณเป็นห่วงได้ถูกปรับปรุงสถานะแล้ว';
+$labels['eventcancelsubject'] = '"$title" ถูกยกเลิก';
+$labels['eventcancelmailbody'] = "*\$title*\n\nเมื่อ: \$date\n\nInvitees: \$attendees\n\n เหตุการณ์ได้ถูกยกเลิกโดย \$organizer.\n\n ไฟล์ iCalendar ที่แนบมาด้วย ได้รับการปรับปรุงรายละเอียดเรียบร้อยแล้ว";
+$labels['itipobjectnotfound'] = 'เหตุการณ์ที่อ้างถึงโดยข้อความนี้ไม่ถูกตรวจพบในปฎิทินของคุณ';
+$labels['itipmailbodyaccepted'] = "\$sender ได้ตอบรับคำเชิญสำหรับเหตุการณ์ต่อไปนี้:\n\n*\$title*\n\nเมื่อ: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender มีแนวโน้มที่จะตอบรับคำเชิญสำหรับเหตุการณ์ต่อไปนี้:\n\n*\$title*\n\nเมื่อ: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender ได้ปฎิเสธคำเชิญสำหรับเหตุการณ์ต่อไปนี้ :\n\n*\$title*\n\nเมื่อ: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender ปฎิเสธการเข้าร่วมของคุณในเหตุการณ์ต่อไปนี้:\n\n*\$title*\n\nเมื่อ: \$date";
+$labels['itipmailbodydelegated'] = "\$sender ได้มอบหมายการเข้าร่วมเหตุการณ์ต่อไปนี้:\n\n*\$title*\n\nเมื่อ: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender ได้มอบหมายให้คุณเข้าร่วมเหตุการณ์ต่อไปนี้:\n\n*\$title*\n\nเมื่อ: \$date";
+$labels['itipdeclineevent'] = 'คุณต้องการปฎิเสธคำเชิญสำหรับเหตุการณ์นี้หรือไม่';
+$labels['declinedeleteconfirm'] = 'คุณต้องการลบเหตุการณ์ที่ปฎิเสธนี้ออกจากปฎิทินของคุณหรือไม่';
+$labels['itipcomment'] = 'ความคิดเห็น คำเชิญ/คำแจ้งเตือน';
+$labels['itipcommenttitle'] = 'ความคิดเห็นนี้จะถูกแนบไปพร้อมกับข้อความ คำเชิญ/คำแจ้งเตือน ไปยังผู้เข้าร่วม';
+$labels['notanattendee'] = 'คุณไม่อยู่ในรายชื่อผุ้เข้าร่วมสำหรับเหตุการณ์นี้';
+$labels['eventcancelled'] = 'เหตุการณ์ได้ถูกยกเลิก';
+$labels['saveincalendar'] = 'บันทึกใน';
+$labels['updatemycopy'] = 'ปรับปรุงปฎิทินของฉัน';
+$labels['savetocalendar'] = 'บันทึกไปยังปฎิทินของฉัน';
+$labels['openpreview'] = 'ตรวจสอบปฎิทิน';
+$labels['noearlierevents'] = 'ไม่มีเหตุการณ์ก่อนหน้า';
+$labels['nolaterevents'] = 'ไม่มีเหตุการณ์หลังจากนั้น';
+$labels['resource'] = 'ทรัพยากร';
+$labels['addresource'] = 'จองทรัพยากร';
+$labels['findresources'] = 'ค้นหาทรัพยากร';
+$labels['resourcedetails'] = 'รายละเอียด';
+$labels['resourceavailability'] = 'ยังว่างอยู่';
+$labels['resourceowner'] = 'เจ้าของ';
+$labels['resourceadded'] = 'ทรัพยากรได้ถูกเพิ่มไปยังเหตุการณ์ของคุณ';
+$labels['tabsummary'] = 'สรุป';
+$labels['tabrecurrence'] = 'การเกิดซ้ำ';
+$labels['tabattendees'] = 'ผู้เข้าร่วม';
+$labels['tabresources'] = 'ทรัพยากร';
+$labels['tabattachments'] = 'สิ่งที่แนบมาด้วย';
+$labels['tabsharing'] = 'แบ่งปัน';
+$labels['deleteobjectconfirm'] = 'คุณต้องการลบเหตุการณ์นี้หรือไม่';
+$labels['deleteventconfirm'] = 'คุณต้องการลบเหตุการณ์นี้หรือไม่';
+$labels['deletecalendarconfirm'] = 'คุณต้องการลบปฎิทินฉบับนี้พร้อมกับเหตุการณ์ทั้งหมดหรือไม่';
+$labels['deletecalendarconfirmrecursive'] = 'คุณต้องการลบปฎิทินฉบับนี้พร้อมเหตุการณ์และปฎิทินย่อยทั้งหมดหรือไม่';
+$labels['savingdata'] = 'บันทึกข้อมูล';
+$labels['errorsaving'] = 'การบันทึกการเปลี่ยนแปลงล้มเหลว';
+$labels['operationfailed'] = 'การทำตามคำร้องล้มเหลว';
+$labels['invalideventdates'] = 'วันที่ที่ป้อนเข้ามาไม่ถูกต้อง โปรดตรวจสอบการป้อนข้อมูลของท่าน';
+$labels['invalidcalendarproperties'] = 'การตั้งค่าคุณสมบัติปฎิทินไม่ถูกต้อง โปรดตั้งชื่อให้ถูกต้อง';
+$labels['searchnoresults'] = 'ไม่พบเหตุการณ์ในปฎิทินฉบับที่เลือก';
+$labels['successremoval'] = 'เหตุการณ์ได้ถูกลบเรียบร้อยแล้ว';
+$labels['successrestore'] = 'การกู้เหตุการณ์เรียบร้อยแล้ว';
+$labels['errornotifying'] = 'เกิดข้อผิดพลาดในการส่งข้อความเตือนไปยังผู้เข้าร่วมเหตุการณ์';
+$labels['errorimportingevent'] = 'การนำเข้าเหตุการณ์เกิดข้อผิดพลาด';
+$labels['importwarningexists'] = 'มีเหตุการณ์นี้อยู่ในปฎิทินของคุณอยู่แล้ว';
+$labels['newerversionexists'] = 'มีเหตุการณ์ที่มีรุ่นควบคุมที่ใหม่กว่าอยู่แล้ว ยกเลิกการนำเข้า';
+$labels['nowritecalendarfound'] = 'ไม่พบปฎิทินที่จะบันทึกเหตุการณ์นี้';
+$labels['importedsuccessfully'] = 'เหตุการณ์ได้ถูกเพิ่มไปยัง \'$calendar\' เรียบร้อยแล้ว';
+$labels['updatedsuccessfully'] = 'เหตุการณ์ได้ถูกปรับปรุงไปยัง \'$calendar\' เรียบร้อยแล้ว';
+$labels['attendeupdateesuccess'] = 'การปรับปรุงสถานะของผู้เข้าร่วมเรียบร้อยแล้ว';
+$labels['itipsendsuccess'] = 'คำเชิญถูกส่งไปยังผู้เข้าร่วมแล้ว';
+$labels['itipresponseerror'] = 'การส่งคำตอบรับสำหรับคำเชิญเหตุการณ์นี้ล้มเหลว';
+$labels['itipinvalidrequest'] = 'คำเชิญนี้ไม่มีผลแล้ว';
+$labels['sentresponseto'] = 'การส่งคำตอบรับสำหรับคำเชิญไปยัง $mailto เรียบร้อยแล้ว';
+$labels['localchangeswarning'] = 'คุณกำลังปรับเปลี่ยนรายละเอียดซึ่งมีผลกับปฎิทินของคุณเท่านั้น และ การปรับเปลี่ยนจะไม่ถูกส่งไปยังผู้จัดของเหตุการณ์นี้';
+$labels['importsuccess'] = 'นำเข้า $nr เหตุการณ์เรียบร้อยแล้ว';
+$labels['importnone'] = 'ไม่พบเหตุการณ์ที่จะนำเข้า';
+$labels['importerror'] = 'พบข้อผิดพลาดในระหว่างนำเข้า';
+$labels['aclnorights'] = 'คุณไม่มีสิทธิของผู้ดูแลระบบของปฎิทินฉบับนี้';
+$labels['changeeventconfirm'] = 'เปลี่ยนเหตุการณ์';
+$labels['removeeventconfirm'] = 'ลบเหตุการณ์';
+$labels['changerecurringeventwarning'] = 'เหตุการณ์นี้เป็นเหตุการณ์ที่เกิดซ้ำ คุณต้องการแก้ไขเฉพาะเหตุการณ์ปัจจุบันเท่านั้น เหตุการณ์ปัจจุบันรวมทั้งในอนาคต หรือ เหตุการณ์ทั้งหมด หรือ บันทึกเป็นเหตุการณ์ใหม่';
+$labels['removerecurringeventwarning'] = 'เหตุการณ์นี้เป็นเหตุการณ์ที่เกิดซ้ำ คุณต้องการลบเฉพาะเหตุการณ์ปัจจุบันเท่านั้น เหตุการณ์ปัจจุบันรวมทั้งในอนาคต หรือ เหตุการณ์ทั้งหมด';
+$labels['removerecurringallonly'] = 'เหตุการณ์นี้เป็นเหตุการณ์ที่เกิดซ้ำ ในฐานะผู้เข้าร่วม คุณสามารถทำการลบได้เฉพาะแบบเหตุการณ์ทั้งหมด พร้อม กำหนดการที่จะเกิดขึ้นทั้งหมดเท่านั้น';
+$labels['currentevent'] = 'ปัจจุบัน';
+$labels['futurevents'] = 'อนาคต';
+$labels['allevents'] = 'ทั้งหมด';
+$labels['saveasnew'] = 'บันทึกเป็นเหตุการณ์ใหม่';
+$labels['birthdays'] = 'วันเกิด';
+$labels['birthdayscalendar'] = 'ปฎิทินวันเกิด';
+$labels['displaybirthdayscalendar'] = 'แสดงปฎิทินวันเกิด';
+$labels['birthdayscalendarsources'] = 'จากสมุดที่อยู่เหล่านี้';
+$labels['birthdayeventtitle'] = 'วันเกิดของ $name';
+$labels['birthdayage'] = 'อายุ $age ปี';
+$labels['objectchangelog'] = 'ประวัติการปรับเปลี่ยน';
+$labels['revision'] = 'รุ่นการปรับปรุง';
+$labels['user'] = 'ผู้ใช้';
+$labels['operation'] = 'การกระทำ';
+$labels['actionappend'] = 'บันทึกเรียบร้อย';
+$labels['actionmove'] = 'ย้ายเรียบร้อย';
+$labels['actiondelete'] = 'ลบเรียบร้อย';
+$labels['compare'] = 'เปรียบเทียบ';
+$labels['showrevision'] = 'แสดงรุ่นการปรับปรุงนี้';
+$labels['restore'] = 'ย้อนกลับไปยังรุ่นการปรับปรุงนี้';
+$labels['objectnotfound'] = 'การโหลดข้อมูลของเหตุการณ์ล้มเหลว';
+$labels['objectchangelognotavailable'] = 'ไม่มีประวัติการปรับเปลี่ยนสำหรับเหตุการณ์นี้';
+$labels['objectdiffnotavailable'] = 'ไม่สามารถเปรียบเทียบรุ่นการปรับปรุงที่เลือกได้';
+$labels['revisionrestoreconfirm'] = 'คุณต้องการกู้คืนรุ่นการปรับปรุง $rev ของเหตุการณ์นี้หรือ นี่จะเป็นการเขียนทับข้อมูลปัจจุบันด้วยข้อมูลที่เก่ากว่า';
+$labels['arialabelcalendarview'] = 'มุมมองปฎิทิน';
+$labels['arialabelquicksearchbox'] = 'ป้อนข้อมูลเพื่อค้นหาเหตุการณ์';
+$labels['arialabelcalsearchform'] = 'ฟอร์มค้นหาปฎิทิน';
+$labels['arialabeleventattendees'] = 'รายชื่อผู้เข้าร่วมเหตุการณ์';
+$labels['arialabeleventresources'] = 'รายการทรัพยากรที่ใช้ในเหตุการณ์';
+$labels['arialabelresourcesearchform'] = 'ฟอร์มค้นหาทรัพยากร';
+$labels['arialabelresourceselection'] = 'ทรัพยากรที่ยังว่าง';
+?>
diff --git a/calendar/localization/uk_UA.inc b/calendar/localization/uk_UA.inc
new file mode 100644
index 0000000..f430667
--- /dev/null
+++ b/calendar/localization/uk_UA.inc
@@ -0,0 +1,228 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = 'Початковий вигляд';
+$labels['time_format'] = 'Формат часу';
+$labels['timeslots'] = 'Проміжків на годину';
+$labels['first_day'] = 'Перший день тижня';
+$labels['first_hour'] = 'Показувати починаючи з';
+$labels['workinghours'] = 'Робочі години';
+$labels['add_category'] = 'Додати категорію';
+$labels['remove_category'] = 'Вилучити категорію';
+$labels['defaultcalendar'] = 'Створити нову подію в';
+$labels['eventcoloring'] = 'Колір події';
+$labels['coloringmode0'] = 'Згідно кольору календаря';
+$labels['coloringmode1'] = 'Згідно кольору категорії';
+$labels['coloringmode2'] = 'Колір календаря для рамки, колір категорії для фону';
+$labels['coloringmode3'] = 'Колір категорії для рамки, колір календаря для фону';
+$labels['afternothing'] = 'Нічого не робити';
+$labels['aftertrash'] = 'Перемістити до смітника';
+$labels['afterdelete'] = 'Вилучити повідомлення';
+$labels['afterflagdeleted'] = 'Відмітити як видалене';
+$labels['aftermoveto'] = 'Перемістити в ...';
+$labels['itipoptions'] = 'Запрошення на події';
+$labels['afteraction'] = 'Після того, як запрошення або повідомлення про його зміну оброблено';
+$labels['calendar'] = 'Календар';
+$labels['calendars'] = 'Календарі';
+$labels['category'] = 'Категорія';
+$labels['categories'] = 'Категорії';
+$labels['createcalendar'] = 'Створити новий календар';
+$labels['editcalendar'] = 'Змінити властивості календаря';
+$labels['name'] = 'Назва';
+$labels['color'] = 'Колір';
+$labels['day'] = 'День';
+$labels['week'] = 'Тиждень';
+$labels['month'] = 'Місяць';
+$labels['agenda'] = 'Розпорядок дня';
+$labels['new'] = 'Новий';
+$labels['new_event'] = 'Нова подія';
+$labels['edit_event'] = 'Редагувати подію';
+$labels['edit'] = 'Редагувати';
+$labels['save'] = 'Зберегти';
+$labels['removelist'] = 'Вилучити зі списку';
+$labels['cancel'] = 'Відмінити';
+$labels['select'] = 'Вибрати';
+$labels['print'] = 'Друк';
+$labels['printtitle'] = 'Друкувати календар';
+$labels['title'] = 'Резюме';
+$labels['description'] = 'Опис';
+$labels['all-day'] = 'весь день';
+$labels['export'] = 'Експорт';
+$labels['exporttitle'] = 'Експорт в iCalendar';
+$labels['exportrange'] = 'Події починаючи з';
+$labels['exportattachments'] = 'Із вкладеннями';
+$labels['customdate'] = 'Спеціальна дата';
+$labels['location'] = 'Розташування';
+$labels['url'] = 'URL';
+$labels['date'] = 'Дата';
+$labels['start'] = 'Початок';
+$labels['starttime'] = 'Час початку';
+$labels['end'] = 'Закінчення';
+$labels['endtime'] = 'Час закінчення';
+$labels['repeat'] = 'Повторити';
+$labels['selectdate'] = 'Виберіть дату';
+$labels['freebusy'] = 'Показати як';
+$labels['free'] = 'Вільний';
+$labels['busy'] = 'Зайнятий';
+$labels['outofoffice'] = 'Поза офісом';
+$labels['tentative'] = 'Невизначений';
+$labels['mystatus'] = 'Мій статус';
+$labels['status'] = 'Статус';
+$labels['status-confirmed'] = 'Підтверджений';
+$labels['status-cancelled'] = 'Відмінений';
+$labels['priority'] = 'Пріоритет';
+$labels['sensitivity'] = 'Конфіденційність';
+$labels['public'] = 'публічна';
+$labels['private'] = 'приватна';
+$labels['confidential'] = 'конфіденційна';
+$labels['alarms'] = 'Нагадування';
+$labels['comment'] = 'Коментарій';
+$labels['created'] = 'Створена';
+$labels['changed'] = 'Змінена';
+$labels['unknown'] = 'Невідомо';
+$labels['eventoptions'] = 'Опції';
+$labels['generated'] = 'створений';
+$labels['eventhistory'] = 'Історія';
+$labels['printdescriptions'] = 'Друк опису';
+$labels['parentcalendar'] = 'Вставити всередину';
+$labels['searchearlierdates'] = '« Шукати події раніше';
+$labels['searchlaterdates'] = 'Шукати події пізніше »';
+$labels['andnmore'] = '$nr більше...';
+$labels['togglerole'] = 'Клікніть для перемикання ролі';
+$labels['createfrommail'] = 'Зберегти як подію';
+$labels['importevents'] = 'Імпортувати подію';
+$labels['importrange'] = 'Події починаючи з';
+$labels['onemonthback'] = '1 місяць назад';
+$labels['nmonthsback'] = '$nr місяця (ів) тому';
+$labels['showurl'] = 'Показати URL календаря';
+$labels['showurldescription'] = 'Використовуйте наступну адресу для перегляду Вашого календаря з інших додатків. Ви можете скопіювати і вставити це в будь-який додаток який підтримує формат iCal.';
+$labels['caldavurldescription'] = 'Скопіюйте цей адрес в клієнт, який підтримує <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> (наприклад, Evolution або Mozilla Thunderbird) для повної синхронізації даного календаря зі своїм комп\'ютером або мобільним пристроєм.';
+$labels['findcalendars'] = 'Знайти календарі...';
+$labels['searchterms'] = 'Умови пошуку';
+$labels['calsearchresults'] = 'Доступні календарі';
+$labels['calendarsubscribe'] = 'Завжди показувати';
+$labels['nocalendarsfound'] = 'Не знайдено календарів';
+$labels['nrcalendarsfound'] = '$nr календарів знайдено';
+$labels['quickview'] = 'Подивитися тільки цей календар';
+$labels['invitationspending'] = 'Очікуючі запрошення';
+$labels['invitationsdeclined'] = 'Відхилені запрошення';
+$labels['changepartstat'] = 'Змінити статус учасника';
+$labels['rsvpcomment'] = 'Текст запрошення';
+$labels['listrange'] = 'Діапазон:';
+$labels['listsections'] = 'Розділити на:';
+$labels['smartsections'] = 'Розумні секції';
+$labels['until'] = 'до';
+$labels['today'] = 'Сьогодні';
+$labels['tomorrow'] = 'Завтра';
+$labels['thisweek'] = 'Поточний тиждень';
+$labels['nextweek'] = 'Наступний тиждень';
+$labels['prevweek'] = 'Попередній тиждень';
+$labels['thismonth'] = 'Поточний місяць';
+$labels['nextmonth'] = 'Наступний місяць';
+$labels['weekofyear'] = 'Тиждень';
+$labels['pastevents'] = 'Минуле';
+$labels['futureevents'] = 'Майбутнє';
+$labels['showalarms'] = 'Показувати нагадування';
+$labels['defaultalarmtype'] = 'Типові налаштування нагадувань';
+$labels['defaultalarmoffset'] = 'Типовий час нагадувань';
+$labels['attendee'] = 'Учасний';
+$labels['role'] = 'Роль';
+$labels['availability'] = 'Доступність';
+$labels['confirmstate'] = 'Статус';
+$labels['addattendee'] = 'Додати учасника';
+$labels['roleorganizer'] = 'Організатор';
+$labels['rolerequired'] = 'Обов\'язковий';
+$labels['roleoptional'] = 'Необов\'язковий';
+$labels['cutypeindividual'] = 'Окремий';
+$labels['cutypegroup'] = 'Група';
+$labels['cutyperesource'] = 'Ресурс';
+$labels['cutyperoom'] = 'Кімната';
+$labels['availfree'] = 'Вільний';
+$labels['availbusy'] = 'Зайнятий';
+$labels['availunknown'] = 'Невідомо';
+$labels['availtentative'] = 'Невизначений';
+$labels['availoutofoffice'] = 'Поза офісом';
+$labels['delegatedto'] = 'Доручено:';
+$labels['delegatedfrom'] = 'Доручено від:';
+$labels['scheduletime'] = 'Знайти доступних';
+$labels['sendinvitations'] = 'Запросити';
+$labels['sendnotifications'] = 'Повідомити учасників про зміни';
+$labels['sendcancellation'] = 'Повідомити учасників про скасування події';
+$labels['reqallattendees'] = 'Необхідні/всі учасники';
+$labels['prevslot'] = 'Попередній час';
+$labels['nextslot'] = 'Наступний час';
+$labels['suggestedslot'] = 'Пропонований час';
+$labels['noslotfound'] = 'Неможливо знайти вільний час';
+$labels['invitationsubject'] = 'Ви запрошені на ';
+$labels['invitationmailbody'] = "*\$title*\n\nКоли: \$date\n\nЗапрошені: \$attendees\n\nУ вкладенні Ви знайдете файл iCalendar з усіма деталями події, який Ви можете імпортувати у Вашу програму-щоденник.";
+$labels['invitationattendlinks'] = "У разі, якщо Ваш поштовий клієнт не підтримує запити iTip, Ви можете використати дане нижче посилання, щоб прийняти або відхилити це запрошення:\n\$url";
+$labels['eventupdatesubject'] = '"$title" була оновлена';
+$labels['eventupdatesubjectempty'] = 'Подія, яка стосується Вас, була оновленна ';
+$labels['eventupdatemailbody'] = "*\$title*\n\nКоли: \$date\n\nЗапрошені: \$attendees\n\nУ вкладенні Ви знайдете файл iCalendar з усіма змінами у події, який Ви можете імпортувати у Вашу програму-щоденник.";
+$labels['eventcancelsubject'] = '"$title" була відмінена';
+$labels['eventcancelmailbody'] = "*\$title*\n\nКоли: \$date\n\nЗапрошені: \$attendees\n\nЦя подія скасована \$organizer.\n\nУ вкладенні Ви знайдете файл iCalendar з усіма змінами у події.";
+$labels['itipobjectnotfound'] = 'Згадану в даному повідомленні подію, не знайдено у Вашому календарі.';
+$labels['itipdeclineevent'] = 'Ви хочете відхилити запрошення на дану подію?';
+$labels['declinedeleteconfirm'] = 'Чи хочете Ви також видалити відхилену подію з вашого календаря?';
+$labels['itipcomment'] = 'Коментар до запрошення/повідомлення';
+$labels['itipcommenttitle'] = 'Даний коментар буде прикріплений до запрошення/повідомленням, надісланого учасникам';
+$labels['notanattendee'] = 'Ви не в списку учасників цієї події';
+$labels['eventcancelled'] = 'Дана подія відмінена';
+$labels['saveincalendar'] = 'зберегти в';
+$labels['updatemycopy'] = 'Оновити в моєму календарі';
+$labels['savetocalendar'] = 'Зберегти в календар';
+$labels['openpreview'] = 'Перевірте календар';
+$labels['noearlierevents'] = 'Немає попередніх подій';
+$labels['nolaterevents'] = 'Немає наступних подій';
+$labels['resource'] = 'Ресурс';
+$labels['addresource'] = 'Зарезервувати ресурс';
+$labels['findresources'] = 'Знайти ресурс';
+$labels['resourcedetails'] = 'Подробиці';
+$labels['resourceavailability'] = 'Доступність';
+$labels['resourceowner'] = 'Власник';
+$labels['resourceadded'] = 'Ресурс доданий у Вашу подію';
+$labels['tabsummary'] = 'Резюме';
+$labels['tabrecurrence'] = 'Повторення';
+$labels['tabattendees'] = 'Учасники';
+$labels['tabresources'] = 'Ресурси';
+$labels['tabattachments'] = 'Вкладення';
+$labels['tabsharing'] = 'Спільне використання';
+$labels['savingdata'] = 'Збереження даних...';
+$labels['errorsaving'] = 'Помилка збереження змін.';
+$labels['changeeventconfirm'] = 'Змінити подію';
+$labels['removeeventconfirm'] = 'Вилучити подію';
+$labels['currentevent'] = 'Поточну';
+$labels['futurevents'] = 'Майбутню';
+$labels['allevents'] = 'Всі';
+$labels['birthdays'] = 'Дні Народження';
+$labels['birthdayscalendar'] = 'Календар Днів Народжень';
+$labels['displaybirthdayscalendar'] = 'Показувати календар Днів Народжень';
+$labels['birthdayscalendarsources'] = 'З даних адресних книг';
+$labels['birthdayeventtitle'] = 'День Народження $name';
+$labels['birthdayage'] = 'Вік $age';
+$labels['objectchangelog'] = 'Історія змін';
+$labels['revision'] = 'Ревізія';
+$labels['user'] = 'Користувач';
+$labels['operation'] = 'Дія';
+$labels['actionappend'] = 'Збережено';
+$labels['actionmove'] = 'Переміщено';
+$labels['actiondelete'] = 'Вилучено';
+$labels['compare'] = 'Порівняти';
+$labels['showrevision'] = 'Показати дану весію';
+$labels['restore'] = 'Відновити дану версію';
+$labels['arialabelminical'] = 'Вибір дати';
+$labels['arialabelcalendarview'] = 'Вигляд календаря';
+$labels['arialabelsearchform'] = 'Форма пошуку подій';
+$labels['arialabelquicksearchbox'] = 'Пошук подій';
+$labels['arialabelcalsearchform'] = 'Форма пошуку календарів';
+$labels['calendaractions'] = 'Дії з календарями';
+$labels['arialabeleventattendees'] = 'Учасники події';
+$labels['arialabeleventresources'] = 'Ресурси подій';
+$labels['arialabelresourcesearchform'] = 'Форма пошуку ресурсів';
+$labels['arialabelresourceselection'] = 'Доступні ресурси';
+?>
diff --git a/calendar/localization/zh_CN.inc b/calendar/localization/zh_CN.inc
new file mode 100644
index 0000000..40f5ee9
--- /dev/null
+++ b/calendar/localization/zh_CN.inc
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Localizations for Kolab Calendar plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
+ */
+$labels['default_view'] = '默认视图';
+$labels['time_format'] = '时间格式';
+$labels['workinghours'] = '工作时间';
+$labels['add_category'] = '增加分类';
+$labels['remove_category'] = '移除分类';
+$labels['aftertrash'] = '移到回收站';
+$labels['afterdelete'] = '删除此信息';
+$labels['afterflagdeleted'] = '被删除标签';
+$labels['aftermoveto'] = '移到...';
+$labels['category'] = '分类';
+$labels['categories'] = '类别';
+$labels['name'] = '名称';
+$labels['color'] = '颜色';
+$labels['day'] = '天';
+$labels['week'] = '周';
+$labels['month'] = '月';
+$labels['agenda'] = '议程表';
+$labels['new'] = '新建';
+$labels['new_event'] = '新建事件';
+$labels['edit_event'] = '编辑事件';
+$labels['edit'] = '编辑';
+$labels['save'] = '保存';
+$labels['removelist'] = '从列表中移除';
+$labels['cancel'] = '取消';
+$labels['select'] = '选择';
+$labels['print'] = '打印';
+$labels['title'] = '汇总';
+$labels['description'] = '描述';
+$labels['export'] = '导出';
+$labels['exporttitle'] = '导出到ICalendar';
+$labels['date'] = '日期';
+$labels['start'] = '开始';
+$labels['starttime'] = '开始时间';
+$labels['end'] = '结束';
+$labels['endtime'] = '结束时间';
+$labels['repeat'] = '循环';
+$labels['selectdate'] = '选择日期';
+$labels['free'] = '空闲';
+$labels['busy'] = '忙碌';
+$labels['outofoffice'] = '外出';
+$labels['tentative'] = '临时';
+$labels['mystatus'] = '我的状态';
+$labels['status'] = '状态';
+$labels['status-confirmed'] = '已确认';
+$labels['status-cancelled'] = '已取消';
+$labels['priority'] = '优先级';
+$labels['sensitivity'] = '隐私';
+$labels['public'] = '公开';
+$labels['private'] = '私人';
+$labels['confidential'] = '保密的';
+$labels['links'] = '参考';
+$labels['alarms'] = '提醒';
+$labels['comment'] = '注释';
+$labels['created'] = '已创建';
+$labels['changed'] = '最后更改';
+$labels['unknown'] = '未知';
+$labels['eventoptions'] = '选项';
+$labels['weekofyear'] = '周';
+$labels['confirmstate'] = '状态';
+$labels['availfree'] = '空闲';
+$labels['availbusy'] = '忙碌';
+$labels['availunknown'] = '未知';
+$labels['availtentative'] = '临时';
+$labels['availoutofoffice'] = '外出';
+$labels['tabsummary'] = '汇总';
+$labels['tabsharing'] = '分享';
+$labels['savingdata'] = '保存数据...';
+?>
diff --git a/calendar/print.js b/calendar/print.js
new file mode 100644
index 0000000..941d04c
--- /dev/null
+++ b/calendar/print.js
@@ -0,0 +1,176 @@
+/**
+ * Print view for the Calendar plugin
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+
+/* calendar plugin printing code */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+
+ // quote html entities
+ var Q = function(str)
+ {
+ return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+ };
+
+ var rc_loading;
+ var showdesc = true;
+ var settings = $.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings);
+
+ // create list of event sources AKA calendars
+ var src, event_sources = [];
+ var add_url = (rcmail.env.search ? '&q='+escape(rcmail.env.search) : '');
+ for (var id in rcmail.env.calendars) {
+ if (!rcmail.env.calendars[id].active)
+ continue;
+
+ source = $.extend({
+ url: "./?_task=calendar&_action=load_events&source=" + escape(id) + add_url,
+ className: 'fc-event-cal-'+id,
+ id: id
+ }, rcmail.env.calendars[id]);
+
+ event_sources.push(source);
+ }
+
+ var viewdate = new Date();
+ if (rcmail.env.date)
+ viewdate.setTime(rcmail.env.date * 1000);
+
+ // initalize the fullCalendar plugin
+ var fc = $('#calendar').fullCalendar({
+ header: {
+ left: '',
+ center: 'title',
+ right: 'agendaDay,agendaWeek,month,table'
+ },
+ aspectRatio: 0.85,
+ ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
+ date: viewdate.getDate(),
+ month: viewdate.getMonth(),
+ year: viewdate.getFullYear(),
+ defaultView: rcmail.env.view,
+ eventSources: event_sources,
+ monthNames : settings['months'],
+ monthNamesShort : settings['months_short'],
+ dayNames : settings['days'],
+ dayNamesShort : settings['days_short'],
+ firstDay : settings['first_day'],
+ firstHour : settings['first_hour'],
+ slotMinutes : 60/settings['timeslots'],
+ timeFormat: {
+ '': settings['time_format'],
+ agenda: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
+ list: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
+ table: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
+ },
+ axisFormat : settings['time_format'],
+ columnFormat: {
+ month: 'ddd', // Mon
+ week: 'ddd ' + settings['date_short'], // Mon 9/7
+ day: 'dddd ' + settings['date_short'], // Monday 9/7
+ list: settings['date_agenda'],
+ table: settings['date_agenda']
+ },
+ titleFormat: {
+ month: 'MMMM yyyy',
+ week: settings['dates_long'],
+ day: 'dddd ' + settings['date_long'],
+ list: settings['dates_long'],
+ table: settings['dates_long']
+ },
+ listSections: rcmail.env.listSections !== undefined ? rcmail.env.listSections : settings['agenda_sections'],
+ listRange: rcmail.env.listRange || settings['agenda_range'],
+ tableCols: ['handle', 'date', 'time', 'title', 'location'],
+ allDayText: rcmail.gettext('all-day', 'calendar'),
+ buttonText: {
+ day: rcmail.gettext('day', 'calendar'),
+ week: rcmail.gettext('week', 'calendar'),
+ month: rcmail.gettext('month', 'calendar'),
+ table: rcmail.gettext('agenda', 'calendar')
+ },
+ listTexts: {
+ until: rcmail.gettext('until', 'calendar'),
+ past: rcmail.gettext('pastevents', 'calendar'),
+ today: rcmail.gettext('today', 'calendar'),
+ tomorrow: rcmail.gettext('tomorrow', 'calendar'),
+ thisWeek: rcmail.gettext('thisweek', 'calendar'),
+ nextWeek: rcmail.gettext('nextweek', 'calendar'),
+ thisMonth: rcmail.gettext('thismonth', 'calendar'),
+ nextMonth: rcmail.gettext('nextmonth', 'calendar'),
+ future: rcmail.gettext('futureevents', 'calendar'),
+ week: rcmail.gettext('weekofyear', 'calendar')
+ },
+ loading: function(isLoading) {
+ rc_loading = rcmail.set_busy(isLoading, 'loading', rc_loading);
+ },
+ // event rendering
+ eventRender: function(event, element, view) {
+ if (view.name != 'month' && view.name != 'table') {
+ var cont = element.find('.fc-event-title');
+ if (event.location) {
+ cont.after('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
+ cont = cont.next();
+ }
+ if (event.description && showdesc) {
+ cont.after('<div class="fc-event-description">' + Q(event.description) + '</div>');
+ }
+/* TODO: create icons black on white
+ if (event.recurrence)
+ element.find('.fc-event-time').append('<i class="fc-icon-recurring"></i>');
+ if (event.alarms)
+ element.find('.fc-event-time').append('<i class="fc-icon-alarms"></i>');
+*/
+ }
+ if (view.name == 'table' && event.description && showdesc) {
+ var cols = element.children().css('border', 0).length;
+ element.after('<tr class="fc-event-row-secondary fc-event"><td colspan="'+cols+'" class="fc-event-description">' + Q(event.description) + '</td></tr>');
+ }
+ },
+ viewDisplay: function(view) {
+ // remove hard-coded hight and make contents visible
+ window.setTimeout(function(){
+ if (view.name == 'table') {
+ $('div.fc-list-content').css('overflow', 'visible').height('auto');
+ }
+ else {
+ $('div.fc-agenda-divider')
+ .next().css('overflow', 'visible').height('auto')
+ .children('div').css('overflow', 'visible').height('auto');
+ }
+ // adjust fixed height if vertical day slots
+ var h = $('table.fc-agenda-slots:visible').height() + $('table.fc-agenda-allday:visible').height() + 4;
+ if (h) $('table.fc-agenda-days td.fc-widget-content').children('div').height(h);
+ }, 20);
+ }
+ });
+
+ // activate settings form
+ $('#propdescription').change(function(){
+ showdesc = this.checked;
+ fc.fullCalendar('render');
+ });
+
+});
diff --git a/calendar/skins/larry/README b/calendar/skins/larry/README
new file mode 100644
index 0000000..d162620
--- /dev/null
+++ b/calendar/skins/larry/README
@@ -0,0 +1,11 @@
+Screendesign by FLINT / Bro fr Gestaltung, Bern, Switzerland
+http://bueroflint.com
+
+
+LICENSE
+-------
+The contents of this folder are subject to the Creative Commons
+Attribution-ShareAlike License. It is allowed to copy, distribute,
+transmit and to adapt the work by keeping credits to the original
+autors in the README file.
+See http://creativecommons.org/licenses/by-sa/3.0/ for details.
diff --git a/calendar/skins/larry/calendar.css b/calendar/skins/larry/calendar.css
new file mode 100644
index 0000000..0669a8f
--- /dev/null
+++ b/calendar/skins/larry/calendar.css
@@ -0,0 +1,2327 @@
+/**
+ * Roundcube Calendar plugin styles for skin "Larry"
+ *
+ * Copyright (c) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ * Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
+ *
+ * The contents are subject to the Creative Commons Attribution-ShareAlike
+ * License. It is allowed to copy, distribute, transmit and to adapt the work
+ * by keeping credits to the original autors in the README file.
+ * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
+ */
+
+body.calendarmain {
+ overflow: hidden;
+}
+
+body.calendarmain #mainscreen {
+ left: 0;
+}
+
+/* overrides for tablets and mobile phones */
+@media screen and (max-device-width: 1024px){
+ body.calendarmain {
+ overflow: visible;
+ }
+
+ body.calendarmain #mainscreen {
+ min-width: 1000px !important;
+ min-height: 520px !important;
+ }
+
+ body.calendarmain #header {
+ min-width: 1020px !important;
+ }
+}
+
+#calendarsidebar {
+ position: absolute;
+ top: 0;
+ left: 10px;
+ bottom: 0;
+ width: 250px;
+}
+
+#datepicker {
+ position: absolute;
+ top: 40px;
+ left: 0;
+ width: 100%;
+ min-height: 190px;
+}
+
+#datepicker .ui-datepicker {
+ width: 100% !important;
+ box-shadow: none;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+}
+
+#datepicker .ui-datepicker td a {
+ padding: 5px 4px;
+ font-size: 12px;
+}
+
+#datepicker td.ui-datepicker-activerange {
+ border-color: #69a2b6;
+}
+
+#datepicker .ui-datepicker-activerange a {
+ color: #185d7a;
+ background: #d9f1fb;
+ background: -moz-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9f1fb), color-stop(100%,#c5e3ee));
+ background: -o-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
+ background: -ms-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
+ background: linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0);
+}
+
+#datepicker .ui-datepicker-days-cell-over a.ui-state-default {
+ color: #fff;
+ border-color: #2fa0c0;
+ background: rgba(73,180,210,0.6);
+ text-shadow: 0px 1px 1px #666;
+ filter: none;
+}
+
+#datepicker .ui-datepicker-activerange a.ui-state-active {
+ color: #fff;
+ background: #00acd4;
+ background: -moz-linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00acd4), color-stop(100%,#008fc7));
+ background: -o-linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ background: -ms-linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ background: linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00acd4', endColorstr='#008fc7', GradientType=0);
+}
+
+#datepicker td.ui-datepicker-week-col {
+ cursor: pointer;
+}
+
+#datepicker .ui-datepicker-title {
+ margin: 2px 2.3em 3px 2.3em;
+}
+
+#datepicker .ui-datepicker .ui-datepicker-prev,
+#datepicker .ui-datepicker .ui-datepicker-next {
+ top: 4px;
+}
+
+#calsidebarsplitter {
+ position: absolute;
+ left: 264px;
+ width: 6px;
+ top: 40px !important;
+ bottom: 0;
+ background: url(images/toggle.gif) -1px 48% no-repeat transparent;
+}
+
+div.sidebarclosed {
+ background-position: -8px 48% !important;
+ cursor: pointer;
+}
+
+#calsidebarsplitter:hover {
+ background-color: #ddd;
+}
+
+#calendar {
+ position: absolute;
+ top: 0;
+ left: 276px;
+ right: 0;
+ bottom: 0;
+}
+
+.calendarmain #message.statusbar {
+ border: 1px solid #c3c3c3;
+ border-bottom-color: #ababab;
+}
+
+#timezonedisplay {
+ position: absolute;
+ bottom: 5px;
+ right: 12px;
+ font-size: 0.85em;
+ color: #666;
+}
+
+#print {
+ width: 680px;
+}
+
+pre {
+ font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+}
+
+#calendars {
+ position: absolute;
+ top: 276px;
+ left: 0;
+ bottom: 0;
+ right: 0;
+}
+
+#calendars .boxtitle {
+ position: relative;
+}
+
+#calendars .boxtitle a.iconbutton.search {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 16px;
+ cursor: pointer;
+ background-position: -2px -317px;
+}
+
+#calendars .listsearchbox {
+ display: none;
+}
+
+#calendars .listsearchbox.expanded {
+ display: block;
+}
+
+#calendars .scroller {
+ top: 34px;
+}
+
+#calendars .listsearchbox.expanded + .scroller {
+ top: 68px;
+}
+
+#calendars .treelist li {
+ margin: 0;
+ position: relative;
+}
+
+#calendars .treelist li div.folder,
+#calendars .treelist li div.calendar {
+ position: relative;
+ height: 28px;
+}
+
+#calendars .treelist li div.virtual {
+ height: 22px;
+}
+
+#calendars .treelist li span.calname {
+ display: block;
+ padding: 0px 18px 2px 2px;
+ position: absolute;
+ top: 7px;
+ left: 38px;
+ right: 45px;
+ cursor: default;
+ background: url(images/calendars.png) right 20px no-repeat;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: #004458;
+}
+
+.quickview-active #calendars .treelist div input,
+.quickview-active #calendars .treelist div .calname {
+ opacity: 0.35;
+}
+
+.quickview-active #calendars .treelist div.focusview .calname {
+ opacity: 1.0;
+ background-image: none;
+}
+
+#calendars .treelist li div.virtual > span.calname {
+ color: #aaa;
+ top: 4px;
+ left: 20px;
+}
+
+#calendars .treelist li.x-birthdays span.calname,
+#calendars .treelist li.x-invitations span.calname {
+ font-style: italic;
+}
+
+#calendars .treelist.flat li span.calname {
+ left: 24px;
+ right: 42px;
+}
+
+#calendars .treelist li span.handle {
+ display: inline-block;
+ position: absolute;
+ top: 8px;
+ right: 6px;
+ padding: 0;
+ width: 10px;
+ height: 10px;
+ border-radius: 7px;
+ font-size: 0.8em;
+ border: 1px solid rgba(0, 0, 0, 0.5);
+ -webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
+ -moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
+ box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
+}
+
+#calendars .treelist div span.actions {
+ display: inline-block;
+ position: absolute;
+ top: 2px;
+ right: 22px;
+ padding: 5px 20px 0 6px;
+/* min-width: 40px; */
+ height: 19px;
+ text-align: right;
+ z-index: 4;
+}
+
+#calendars .treelist div:hover span.actions {
+ top: 1px;
+ right: 21px;
+ border: 1px solid #c6c6c6;
+ border-radius: 4px;
+ background: #f7f7f7;
+ background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
+ background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f9f9f9', endColorstr='#e6e6e6', GradientType=0);
+}
+
+#calendars .treelist li a.subscribed {
+ display: inline-block;
+ position: absolute;
+ top: 5px;
+ right: 3px;
+ height: 16px;
+ width: 16px;
+ padding: 0;
+ background: url(images/calendars.png) -100px 0 no-repeat;
+ overflow: hidden;
+ text-indent: -5000px;
+ cursor: pointer;
+}
+
+#calendars .treelist div:hover a.subscribed,
+#calendars .treelist div a.subscribed:focus {
+ background-position: 0 -110px;
+}
+
+#calendars .treelist div.subscribed a.subscribed,
+#calendars .treelist div.subscribed a.subscribed:focus {
+ background-position: -16px -110px;
+}
+
+#calendars .treelist div.subscribed.partial a.subscribed,
+#calendars .treelist div.subscribed.partial a.subscribed:focus {
+ background-position: -16px -148px;
+}
+
+#calendars .treelist div a.remove:focus,
+#calendars .treelist div a.quickview:focus,
+#calendars .treelist div a.subscribed:focus {
+ border-radius: 3px;
+ outline: 2px solid rgba(30,150,192, 0.5);
+}
+
+#calendars .treelist div a.remove,
+#calendars .treelist div a.quickview {
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-right: 4px;
+ padding: 0;
+ background: url(images/calendars.png) -100px 0 no-repeat;
+ overflow: hidden;
+ text-indent: -5000px;
+ cursor: pointer;
+}
+
+#calendars .treelist div a.quickview:focus,
+#calendars .treelist div:hover a.quickview {
+ background-position: 0 -128px;
+ background-color: transparent !important;
+}
+
+#calendars .treelist div.focusview a.quickview {
+ background-position: -16px -128px;
+}
+
+#calendars .treelist div a.remove:focus,
+#calendars .treelist div:hover a.remove {
+ background-position: -16px -168px;
+ background-color: transparent !important;
+}
+
+#calendars .searchresults .treelist div a.remove {
+ display: none;
+}
+
+#calendars .treelist li input {
+ position: absolute;
+ top: 5px;
+ left: 18px;
+}
+
+#calendars .treelist li div.treetoggle {
+ top: 8px;
+}
+
+#calendars .treelist li.virtual div.treetoggle {
+ top: 6px;
+}
+
+#calendars .treelist.flat li input {
+ left: 4px;
+}
+
+#calendars .treelist ul li div.folder,
+#calendars .treelist ul li div.calendar {
+ margin-left: 16px;
+}
+
+#calendars .treelist ul ul li div.folder,
+#calendars .treelist ul ul li div.calendar {
+ margin-left: 32px;
+}
+
+#calendars .treelist ul ul ul li div.folder,
+#calendars .treelist ul ul ul li div.calendar {
+ margin-left: 48px;
+}
+
+#calendars .treelist li.selected > div.calendar {
+ background-color: #c7e3ef;
+}
+
+#calendars .treelist li.selected > span.calname {
+ font-weight: bold;
+}
+
+#calendars .treelist div.readonly span.calname {
+ background-position: right -20px;
+}
+
+#calendars .treelist li.user > div > span.calname {
+ background-position: right -38px;
+}
+/*
+#calendars .treelist div.user.readonly span.calname {
+ background-position: right -56px;
+}
+
+#calendars .treelist div.shared span.calname {
+ background-position: right -74px;
+}
+
+#calendars .treelist div.shared.readonly span.calname {
+ background-position: right -92px;
+}
+*/
+
+#calendars .treelist .calendar .count {
+ position: absolute;
+ display: inline-block;
+ top: 5px;
+ right: 68px;
+ min-width: 1.3em;
+ padding: 2px 4px;
+ background: #005d76;
+ background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
+ background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
+ background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
+ background: linear-gradient(to bottom, #005d76 0%, #004558 100%);
+ -webkit-box-shadow: inset 0 1px 1px 0 #002635;
+ box-shadow: inset 0 1px 1px 0 #002635;
+ border-radius: 10px;
+ color: #fff;
+ text-align: center;
+ font-style: normal;
+ font-weight: bold;
+ text-shadow: none;
+ z-index: 3;
+}
+
+#calendars .searchresults {
+ background: #b0ccd7;
+ margin-top: 8px;
+}
+
+#calendars .searchresults .boxtitle {
+ background: none;
+ padding: 2px 8px 2px 8px;
+}
+
+#calendars .searchresults .listing li {
+ background-color: #c7e3ef;
+}
+
+#calfeedurl,
+#caldavurl {
+ width: 98%;
+ background: #fbfbfb;
+ padding: 4px;
+ margin-bottom: 1em;
+ resize: none;
+}
+
+#agendalist {
+ width: 100%;
+ margin: 0 auto;
+ margin-top: 60px;
+ border: 1px solid #C1DAD7;
+ display: none;
+}
+
+#agendalist table {
+ width: 100%;
+}
+
+#agendalist td,
+#agendalist th {
+ border-right: 1px solid #C1DAD7;
+ border-bottom: 1px solid #C1DAD7;
+ background: #fff;
+ padding: 6px 6px 6px 12px;
+}
+
+#agendalist tr {
+ vertical-align: top;
+}
+
+#agendalist th {
+ font-weight: bold;
+}
+
+#calendartoolbar {
+ position: absolute;
+ top: -6px;
+ left: 0;
+ height: 40px;
+ white-space: nowrap;
+}
+
+#calendartoolbar a.button {
+ background-image: url(images/toolbar.png);
+ padding-left: 0;
+ padding-right: 0;
+ min-width: 50px;
+ max-width: 60px;
+}
+
+#calendartoolbar a.button.addevent {
+ background-position: center 1px;
+ max-width: 70px;
+}
+
+#calendartoolbar a.button.export {
+ background-position: center -40px;
+}
+
+#calendartoolbar a.button.import {
+ background-position: center -440px;
+}
+
+#calendartoolbar a.button.print {
+ background-position: center -80px;
+}
+
+body.calendarmain #quicksearchbar {
+ z-index: 20;
+}
+
+body.calendarmain #searchmenulink {
+ width: 15px;
+}
+
+.calendarmain div.uidialog {
+ display: none;
+}
+
+#user {
+ position: absolute;
+ top: 10px;
+ right: 100px;
+ left: 100px;
+ text-align: center;
+}
+
+a.morelink {
+ font-size: 90%;
+ color: #0069a6;
+ text-decoration: none;
+}
+
+a.morelink:hover {
+ text-decoration: underline;
+}
+
+a.miniColors-trigger {
+ margin-top: -3px;
+}
+
+.calendar.attachmentwin #attachmenttoolbar {
+ position: relative;
+ top: -6px;
+ height: 40px;
+}
+
+.calendar.attachmentwin #attachmentcontainer {
+ position: absolute;
+ top: 0;
+ left: 232px;
+ right: 0;
+ bottom: 0;
+}
+
+.calendar.attachmentwin #attachmentframe {
+ width: 100%;
+ height: 100%;
+ border: 0;
+ background-color: #fff;
+ border-radius: 4px;
+}
+
+.calendar.attachmentwin #partheader {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 220px;
+ bottom: 0;
+}
+
+.calendar.attachmentwin #partheader table {
+ table-layout: fixed;
+ overflow: hidden;
+}
+
+.calendar.attachmentwin #partheader table td {
+ color: #666;
+ padding: 4px 6px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.calendar.attachmentwin #partheader table td.header {
+ font-weight: bold;
+}
+
+.calendar.attachmentwin #partheader table td.title {
+ width: 60px;
+ padding-right: 0;
+}
+
+#edit-attachments {
+ margin: 0.6em 0;
+}
+
+#edit-attachments ul li {
+ display: block;
+ color: #333;
+ font-weight: bold;
+ padding: 4px 4px 3px 30px;
+ text-shadow: 0px 1px 1px #fff;
+ text-decoration: none;
+ white-space: nowrap;
+ line-height: 20px;
+}
+
+#edit-attachments ul li a.file {
+ padding: 0;
+}
+
+#edit-attachments-form {
+ margin-top: 1em;
+ padding-top: 0.8em;
+ border-top: 2px solid #fafafa;
+}
+
+#edit-attachments-form .formbuttons {
+ margin: 0.5em 0;
+}
+
+#eventedit .droptarget {
+ background-image: url(../../../../skins/larry/images/filedrop.png) !important;
+ background-position: center bottom !important;
+ background-repeat: no-repeat !important;
+}
+
+#eventedit .droptarget.hover,
+#eventedit .droptarget.active {
+ border-color: #019bc6;
+ box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+ -moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+ -webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+ -o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+}
+
+#eventedit .droptarget.hover {
+ background-color: #d9ecf4;
+ box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+ -moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+ -webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+ -o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+}
+
+#event-attachments .attachmentslist li {
+ float: left;
+ margin-right: 1em;
+}
+
+#event-attachments .attachmentslist li a {
+ outline: none;
+}
+
+#event-panel-attachments.disabled .attachmentslist li a.delete {
+ visibility: hidden;
+}
+
+.event-attendees span.attendee {
+ padding-right: 18px;
+ margin-right: 0.5em;
+ background: url(images/attendee-status.png) right 0 no-repeat;
+}
+
+.event-attendees span.attendee a.mailtolink {
+ text-decoration: none;
+ white-space: nowrap;
+ outline: none;
+}
+
+.event-attendees span.attendee a.mailtolink:hover {
+ text-decoration: underline;
+}
+
+.event-attendees span.accepted {
+ background-position: right -20px;
+}
+
+.event-attendees span.declined {
+ background-position: right -40px;
+}
+
+.event-attendees span.tentative {
+ background-position: right -60px;
+}
+
+.event-attendees span.delegated {
+ background-position: right -180px;
+}
+
+.event-attendees span.organizer {
+ background-position: right -80px;
+}
+
+#all-event-attendees span.attendee {
+ display: block;
+ margin-bottom: 0.4em;
+ padding-bottom: 0.3em;
+ border-bottom: 1px solid #ddd;
+}
+
+.calendarmain .fc-view-table td.fc-list-header,
+#attendees-freebusy-table h3.boxtitle,
+#schedule-freebusy-times thead th,
+.edit-attendees-table thead th
+ {
+ color: #69939e;
+ font-size: 11px;
+ font-weight: bold;
+ background: #d6eaf3;
+ background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
+ background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3));
+ background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
+ background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
+ background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
+ border: 0;
+ border-bottom: 1px solid #ccc;
+ height: 18px;
+ line-height: 18px;
+ padding: 8px 7px 3px 7px;
+}
+
+/* jQuery UI overrides */
+
+.calendarmain .eventdialog h1 {
+ font-size: 18px;
+ margin: -0.3em 0 0.4em 0;
+}
+
+.calendarmain .eventdialog label,
+.calendarmain .eventdialog h5.label {
+ font-weight: normal;
+ font-size: 1em;
+ color: #999;
+ margin: 0 0 0.2em 0;
+}
+
+.calendarmain .eventdialog label span.index,
+.calendarmain .eventdialog h5.label .index {
+ vertical-align: inherit;
+ margin-left: 0.6em;
+}
+
+.calendarmain .eventdialog {
+ margin: 0 -0.2em;
+}
+
+.calendarmain .eventdialog.status-cancelled {
+ background: url(images/badge_cancelled.png) top right no-repeat;
+}
+
+.calendarmain .eventdialog.sensitivity-private {
+ background: url(images/badge_private.png) top right no-repeat;
+}
+
+.calendarmain .eventdialog.sensitivity-confidential {
+ background: url(images/badge_confidential.png) top right no-repeat;
+}
+
+.calendarmain .sensitivity-private #event-title {
+ margin-right: 50px;
+}
+
+.calendarmain .sensitivity-confidential #event-title {
+ margin-right: 60px;
+}
+
+.calendarmain .eventdialog div.event-line {
+ margin-top: 0.1em;
+ margin-bottom: 0.3em;
+}
+
+.calendarmain .eventdialog div.event-line a.iconbutton {
+ margin-left: 0.5em;
+ line-height: 17px;
+}
+
+.calendarmain .eventdialog div.event-line span.event-text + label {
+ margin-left: 2em;
+}
+
+.calendarmain .eventdialog #event-rsvp-comment,
+.calendarmain .eventdialog #event-created-changed {
+ margin-top: 0.6em;
+}
+
+.eventdialog .event-text-old,
+.eventdialog .event-text-new,
+.eventdialog .event-text-diff {
+ padding: 2px;
+}
+
+.eventdialog .event-text-diff del,
+.eventdialog .event-text-diff ins {
+ text-decoration: none;
+ color: inherit;
+}
+
+.eventdialog .event-text-old,
+.eventdialog .event-text-diff del {
+ background-color: #fdd;
+ /* text-decoration: line-through; */
+}
+
+.eventdialog .event-text-new,
+.eventdialog .event-text-diff ins {
+ background-color: #dfd;
+}
+
+#eventdiff .attachmentslist li a,
+#eventdiff .attachmentslist li a:hover {
+ cursor: default;
+ text-decoration: none;
+}
+
+.changelog-table .loading {
+ color: #666;
+ margin: 1em 0;
+ padding: 1px 0 2px 24px;
+ background: url(images/loading_blue.gif) top left no-repeat;
+}
+
+.changelog-dialog .compare-button {
+ margin: 4px 0;
+}
+
+.changelog-table tbody td {
+ padding: 4px 7px;
+ vertical-align: middle;
+}
+
+.changelog-table tbody tr:last-child td {
+ border-bottom: 0;
+}
+
+.changelog-table tbody tr.undisclosed td.date,
+.changelog-table tbody tr.undisclosed td.user {
+ font-style: italic;
+}
+
+.changelog-table .diff {
+ width: 4em;
+ padding: 2px;
+}
+
+.changelog-table .revision {
+ width: 6em;
+}
+
+.changelog-table .date {
+ width: 11em;
+}
+
+.changelog-table .user {
+ width: auto;
+}
+
+.changelog-table .operation {
+ width: 15%;
+}
+
+.changelog-table .actions {
+ width: 50px;
+ text-align: right;
+ padding: 4px;
+}
+
+.changelog-table td a.iconbutton.restore,
+.changelog-table td a.iconbutton.preview {
+ width: 16px;
+ margin-right: 2px;
+ background-image: url(images/calendars.png);
+ background-position: -1px -147px;
+}
+
+.changelog-table td a.iconbutton.restore {
+ background-image: url(images/calendars.png);
+ background-position: -1px -167px;
+}
+
+.changelog-table tr.first td a.iconbutton {
+ opacity: 0.3;
+ cursor: default;
+}
+
+#event-partstat .changersvp {
+ cursor: pointer;
+ color: #333;
+ text-decoration: none;
+}
+
+#event-partstat .iconbutton {
+ visibility: hidden;
+}
+
+#event-partstat .changersvp:focus .iconbutton,
+#event-partstat:hover .iconbutton {
+ visibility: visible;
+}
+
+#eventedit {
+ position: relative;
+ top: -1.5em;
+ padding: 0.5em 0.1em;
+ margin: 0 -0.2em;
+}
+
+#eventedit input.text,
+#eventedit textarea {
+ width: 97%;
+}
+
+#eventtabs {
+ position: relative;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+}
+
+div.form-section,
+.calendarmain .eventdialog div.event-section,
+#eventtabs div.event-section {
+ margin-top: 0.2em;
+ margin-bottom: 0.6em;
+}
+
+#eventtabs .border-after {
+ padding-bottom: 0.8em;
+ margin-bottom: 0.8em;
+ border-bottom: 2px solid #fafafa;
+}
+
+.calendarmain .eventdialog label,
+#eventedit label,
+.form-section label {
+ display: inline-block;
+ min-width: 7em;
+ padding-right: 0.5em;
+}
+
+.calendarmain .eventdialog #event-url .event-text {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+#event-links .attachmentslist {
+ display: inline-block;
+}
+
+#event-links label,
+#edit-event-links label {
+ float: left;
+ margin-top: 0.3em;
+ padding-right: 0.75em;
+}
+
+#edit-event-links .event-text {
+ margin-left: 8em;
+ min-height: 22px;
+}
+
+#edit-event-links .attachmentslist li.message a.messagelink,
+#event-links .attachmentslist li.message a.messagelink {
+ padding: 0 0 0 24px;
+}
+
+#edit-event-links .attachmentslist li a.delete {
+ top: 0;
+ background-position: -6px -378px;
+}
+
+#edit-event-links .attachmentslist li.deleted a.messagelink,
+#edit-event-links .attachmentslist li.deleted a.messagelink:hover {
+ text-decoration: line-through;
+}
+
+#eventedit .formtable td.label {
+ min-width: 6em;
+}
+
+td.topalign {
+ vertical-align: top;
+}
+
+#eventedit label.weekday,
+#eventedit label.monthday {
+ min-width: 3em;
+}
+
+#eventedit label.month {
+ min-width: 5em;
+}
+
+#eventedit .formtable td {
+ padding: 0.2em 0;
+}
+
+.ui-dialog .event-update-confirm {
+ padding: 0 0.5em 0.5em 0.5em;
+}
+
+.event-dialog-message,
+.event-update-confirm .message {
+ margin-top: 0.5em;
+ padding: 0.8em;
+ border: 1px solid #ffdf0e;
+ background-color: #fef893;
+}
+
+.event-dialog-message .message,
+.event-update-confirm .message {
+ margin-bottom: 0.5em;
+}
+
+.edit-recurring-warning .savemode {
+ padding-left: 20px;
+}
+
+.event-update-confirm .savemode {
+ padding-left: 30px;
+}
+
+.event-dialog-message span.ui-icon,
+.event-update-confirm span.ui-icon {
+ float: left;
+ margin: 0 7px 20px 0;
+}
+
+.event-dialog-message label,
+.event-update-confirm label {
+ min-width: 3em;
+ padding-right: 1em;
+}
+
+.event-update-confirm a.button {
+ margin: 0 0.5em 0 0.2em;
+ min-width: 5em;
+ text-align: center;
+}
+
+.libcal-rsvp-replymode li a {
+ cursor: default;
+}
+
+#event-rsvp,
+#edit-attendees-notify {
+ margin: 0.6em 0 0.3em 0;
+ padding: 0.5em;
+}
+
+#event-rsvp .itip-reply-controls {
+ margin-top: 0.5em;
+}
+
+#event-rsvp .itip-reply-controls label {
+ color: #333;
+}
+
+#event-rsvp .itip-reply-controls textarea {
+ width: 95%;
+}
+
+#eventedit .edit-attendees-table {
+ width: 100%;
+ margin-top: 0.5em;
+}
+
+#eventedit .edit-attendees-table th.role,
+#eventedit .edit-attendees-table td.role {
+ width: 9em;
+}
+
+#eventedit .edit-attendees-table th.availability,
+#eventedit .edit-attendees-table td.availability,
+#eventedit .edit-attendees-table th.confirmstate,
+#eventedit .edit-attendees-table td.confirmstate {
+ width: 4em;
+}
+
+#eventedit .edit-attendees-table th.options,
+#eventedit .edit-attendees-table td.options {
+ width: 16px;
+ padding: 2px 4px;
+}
+
+#eventedit .edit-attendees-table th.invite,
+#eventedit .edit-attendees-table td.invite {
+ width: 44px;
+ padding: 2px;
+}
+
+#eventedit .edit-attendees-table th.invite label {
+ display: inline-block;
+ position: relative;
+ top: 4px;
+ width: 24px;
+ height: 18px;
+ min-width: 24px;
+ padding: 0;
+ overflow: hidden;
+ text-indent: -5000px;
+ white-space: nowrap;
+ background: url(images/sendinvitation.png) 1px 0 no-repeat;
+}
+
+#eventedit .edit-attendees-table tbody tr:last-child td {
+ border-bottom: 0;
+}
+
+#eventedit .edit-attendees-table th.name,
+#eventedit .edit-attendees-table td.name {
+ width: auto;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#eventedit .edit-attendees-table td.name select {
+ width: 100%;
+}
+
+#eventedit .edit-attendees-table td.name .attendee-name {
+ display: block;
+ position: relative;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 5px 7px 6px;
+ margin: -5px -7px -6px;
+}
+
+#eventedit .edit-attendees-table a.deletelink {
+ display: inline-block;
+ width: 17px;
+ height: 17px;
+ padding: 0;
+ overflow: hidden;
+ text-indent: 1000px;
+}
+
+#eventedit .edit-attendees-table a.expandlink {
+ position: absolute;
+ top: 4px;
+ right: 6px;
+ width: 16px;
+ height: 16px;
+}
+
+#edit-attendees-form,
+#edit-resources-form {
+ position: relative;
+ margin-top: 15px;
+}
+
+#edit-attendees-form .attendees-invitebox {
+ text-align: right;
+ margin: 0;
+}
+
+#edit-attendees-form .attendees-invitebox label {
+ padding-right: 3px;
+}
+
+#edit-resources-form #edit-resource-find {
+ position: absolute;
+ top: 0;
+ right: 0;
+}
+
+#edit-attendees-form #edit-attendee-schedule {
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.edit-attendees-table select.edit-attendee-role {
+ border: 0;
+ padding: 2px;
+ background: white;
+ width: 100%;
+}
+
+.availability img.availabilityicon {
+ margin: 1px;
+ width: 14px;
+ height: 14px;
+ border-radius: 4px;
+ -moz-border-radius: 4px;
+}
+
+.availability img.availabilityicon.loading {
+ background: url(images/loading_blue.gif) center no-repeat;
+}
+
+#schedule-freebusy-times td.unknown,
+.availability img.availabilityicon.unknown {
+ background: #ddd;
+}
+
+#schedule-freebusy-times td.free,
+.availability img.availabilityicon.free {
+ background: #abd640;
+}
+
+#schedule-freebusy-times td.busy,
+.availability img.availabilityicon.busy {
+ background: #e26569;
+}
+
+#schedule-freebusy-times td.tentative,
+.availability img.availabilityicon.tentative {
+ background: #8383fc;
+}
+
+#schedule-freebusy-times td.out-of-office,
+.availability img.availabilityicon.out-of-office {
+ background: #fbaa68;
+}
+
+#schedule-freebusy-times td.all-busy,
+#schedule-freebusy-times td.all-tentative,
+#schedule-freebusy-times td.all-out-of-office {
+ background-image: url(images/freebusy-colors.png);
+ background-position: top right;
+ background-repeat: no-repeat;
+}
+
+#schedule-freebusy-times td.all-tentative {
+ background-position: right -40px;
+}
+
+#schedule-freebusy-times td.all-out-of-office {
+ background-position: right -80px;
+}
+
+#edit-attendees-legend {
+ margin-top: 3em;
+ margin-bottom: 0.5em;
+}
+
+#edit-attendees-legend .legend {
+ margin-right: 2em;
+ white-space: nowrap;
+}
+
+#edit-attendees-legend img.availabilityicon {
+ vertical-align: middle;
+}
+
+.edit-attendees-table tbody td.confirmstate {
+ overflow: hidden;
+ white-space: nowrap;
+ text-indent: -2000%;
+}
+
+.edit-attendees-table td.confirmstate span {
+ display: block;
+ width: 20px;
+ background: url(images/attendee-status.png) 5px 0 no-repeat;
+}
+
+.edit-attendees-table td.confirmstate span.needs-action {
+}
+
+.edit-attendees-table td.confirmstate span.accepted {
+ background-position: 5px -20px;
+}
+
+.edit-attendees-table td.confirmstate span.declined {
+ background-position: 5px -40px;
+}
+
+.edit-attendees-table td.confirmstate span.tentative {
+ background-position: 5px -60px;
+}
+
+.edit-attendees-table td.confirmstate span.delegated {
+ background-position: 5px -180px;
+}
+
+#attendees-freebusy-table {
+ width: 100%;
+ table-layout: fixed;
+ border: 1px solid #bbd3da;
+}
+
+#attendees-freebusy-table td.attendees {
+ width: 18em;
+ vertical-align: top;
+ overflow: hidden;
+}
+
+#attendees-freebusy-table td.times {
+ width: auto;
+ vertical-align: top;
+}
+
+#attendees-freebusy-table div.scroll {
+ position: relative;
+ overflow: auto;
+}
+
+#attendees-freebusy-table h3.boxtitle {
+ margin: 0;
+ border-color: #ccc;
+}
+
+.attendees-list .attendee {
+ padding: 4px 4px 4px 1px;
+ background: url(images/attendee-status.png) 2px -97px no-repeat;
+ white-space: nowrap;
+}
+
+.attendees-list a.attendee-role-toggle {
+ display: inline-block;
+ width: 16px;
+ margin-right: 3px;
+ cursor: pointer;
+}
+
+.attendees-list div.attendee {
+ border-top: 1px solid #ccc;
+}
+
+.attendees-list span.attendee {
+ padding-left: 20px;
+ margin-right: 2em;
+}
+
+.attendees-list .organizer {
+ background-position: 3px -77px;
+}
+
+.attendees-list .opt-participant {
+ background-position: 2px -117px;
+}
+
+.attendees-list .non-participant {
+ background-position: 2px -137px;
+}
+
+.attendees-list .chair {
+ background-position: 2px -157px;
+}
+
+.attendees-list .loading {
+ background: url(images/loading_blue.gif) 1px 50% no-repeat;
+}
+
+.attendees-list .total {
+ background: none;
+ padding-left: 4px;
+ font-weight: bold;
+}
+
+.attendees-list .spacer,
+#schedule-freebusy-times tr.spacer td {
+ background: 0;
+ font-size: 50%;
+}
+
+#schedule-freebusy-times {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+#schedule-freebusy-times td {
+ padding: 4px;
+ border: 1px solid #ccc;
+}
+
+#attendees-freebusy-table div.timesheader,
+#schedule-freebusy-times tr.times td {
+ min-width: 30px;
+ font-size: 9px;
+ padding: 5px 2px 6px 2px;
+ text-align: center;
+ color: #004658;
+}
+
+#schedule-freebusy-times tr.times td.allday {
+ min-width: 60px;
+}
+
+#schedule-freebusy-times tr.times td {
+ cursor: pointer;
+}
+
+#schedule-event-time {
+ position: absolute;
+ border: 2px solid #333;
+ background: #777;
+ background: rgba(60, 60, 60, 0.6);
+ opacity: 0.5;
+ border-radius: 4px;
+ cursor: move;
+ filter: alpha(opacity=40); /* IE8 */
+}
+
+#eventfreebusy .schedule-options {
+ position: relative;
+ margin-bottom: 1.5em;
+}
+
+#eventfreebusy .schedule-buttons {
+ position: absolute;
+ top: 0.5em;
+ right: 0;
+ margin-right: 0;
+}
+
+#eventfreebusy .schedule-find-buttons {
+ padding-bottom:0.5em;
+}
+
+#eventfreebusy .schedule-find-buttons button {
+ min-width: 9em;
+ text-align: center;
+}
+
+#eventedit .attendees-commentbox label {
+ display: block;
+}
+
+#eventedit .ui-tabs-panel {
+ min-height: 24em;
+}
+
+#rcmKSearchpane ul li.resource i.icon,
+#rcmKSearchpane ul li.collection i.icon {
+ background-image: url(images/autocomplete.png);
+ background-position: -1px -2px;
+}
+
+#rcmKSearchpane ul li.collection i.icon {
+ background-position: -1px -26px;
+}
+
+a.dropdown-link {
+ font-size: 11px;
+ text-decoration: none;
+}
+
+a.dropdown-link:after {
+ content: ' ▼';
+ font-size: 10px;
+ color: #666;
+}
+
+.ui-dialog-buttonset a.dropdown-link {
+ position: relative;
+ top: 2px;
+ margin: 0 1em;
+ color: #333;
+}
+
+#calendarsidebar .ui-datepicker-calendar {
+ table-layout: fixed;
+}
+
+.ui-datepicker-calendar .ui-datepicker-week-col {
+ border: 0;
+ color: #999;
+ font-size: 90%;
+ text-align: right;
+ padding-right: 6px;
+ width: 20px;
+ overflow: hidden;
+}
+
+.ui-autocomplete {
+ max-height: 160px;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+.ui-autocomplete .ui-menu-item {
+ white-space: nowrap;
+}
+
+* html .ui-autocomplete {
+ height: 160px;
+}
+
+.calendarmain span.spacer {
+ padding-left: 3em;
+}
+
+#agendaoptions {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: auto;
+ z-index: 10;
+ padding: 4px 5px;
+ border: 1px solid #c3c3c3;
+ border-top-color: #ddd;
+ border-bottom-color: #bbb;
+ border-radius: 0 0 4px 4px;
+ background: #ebebeb;
+ background: -moz-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ebebeb), color-stop(100%,#c6c6c6));
+ background: -o-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
+ background: -ms-linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
+ background: linear-gradient(top, #ebebeb 0%, #c6c6c6 100%);
+}
+
+#agendaoptions label {
+ text-shadow: 1px 1px #fff;
+ padding-right: 0.5em;
+}
+
+#calendar-kolabform {
+ position: relative;
+ margin: 0 -8px;
+ min-width: 660px;
+ min-height: 400px;
+}
+
+#calendar-kolabform table td.title {
+ font-weight: bold;
+ white-space: nowrap;
+ color: #666;
+ padding-right: 10px;
+}
+
+#resource-selection {
+ position: absolute;
+ top: 0;
+ left: 8px;
+ right: 0;
+ bottom: 0;
+}
+
+#resource-selection .scroller {
+ top: 34px;
+}
+
+#resource-dialog-left {
+ position: absolute;
+ top: 10px;
+ left: 0;
+ width: 380px;
+ bottom: 10px;
+}
+
+#resource-dialog-right {
+ position: absolute;
+ top: 10px;
+ left: 392px;
+ right: 8px;
+ bottom: 10px;
+}
+
+#resource-info {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 48%;
+}
+
+#resource-info table {
+ margin: 8px;
+ width: 97%;
+}
+
+#resource-info thead td {
+ background: none;
+ font-weight: bold;
+ font-size: 14px;
+}
+
+#resource-availability {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 49%;
+}
+
+#resource-freebusy-calendar {
+ position: absolute;
+ top: 33px;
+ left: -1px;
+ right: -1px;
+ bottom: -1px;
+}
+
+#resource-freebusy-calendar .fc-content {
+ top: 0;
+}
+
+#resource-freebusy-calendar .fc-content .fc-event-bg {
+ background: 0;
+}
+
+#resource-freebusy-calendar .fc-event.status-busy,
+#resource-freebusy-calendar .status-busy .fc-event-skin {
+ border-color: #e26569;
+ background-color: #e26569;
+}
+
+#resource-freebusy-calendar .fc-event.status-tentative,
+#resource-freebusy-calendar .status-tentative .fc-event-skin {
+ border-color: #8383fc;
+ background: #8383fc;
+}
+
+#resource-freebusy-calendar .fc-event.status-outofoffice,
+#resource-freebusy-calendar .status-outofoffice .fc-event-skin {
+ border-color: #fbaa68;
+ background: #fbaa68;
+}
+
+#resourcequicksearch {
+ padding: 4px;
+ background: #c7e3ef;
+}
+
+#resourcesearchbox {
+ width: 100%;
+ height: 26px;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+#resourcequicksearch .iconbutton.searchoptions {
+ position: absolute;
+ top: 5px;
+ left: 6px;
+ width: 16px;
+}
+
+.searchbox .iconbutton.reset {
+ position: absolute;
+ top: 4px;
+ right: 1px;
+}
+
+
+
+/* fullcalendar style overrides */
+
+.rcube-fc-content {
+ overflow: hidden;
+ border: 0;
+ border-radius: 4px;
+ box-shadow: 0 0 2px #999;
+ -o-box-shadow: 0 0 2px #999;
+ -webkit-box-shadow: 0 0 2px #999;
+ -moz-box-shadow: 0 0 2px #999;
+}
+
+.calendarmain .fc-content {
+ position: absolute !important;
+ top: 40px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: #fff;
+}
+
+.calendarmain.quickview-active .fc-content {
+ background-image: url('images/focusview.png');
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+#fish-eye-view .fc-content {
+ top: 2px;
+ bottom: 2px;
+}
+
+#quickview-calendar {
+ padding: 8px;
+ overflow: hidden;
+}
+
+.calendarmain .fc-button,
+.calendarmain .fc-button.fc-state-default,
+.calendarmain .fc-button.fc-state-hover {
+ background-color: #f5f5f5;
+ background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
+ background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
+ background-position: 0 0;
+}
+
+.calendarmain #calendar .fc-button,
+.calendarmain #calendar .fc-button.fc-state-default,
+.calendarmain #calendar .fc-button.fc-state-hover {
+ margin: 0 0 0 0;
+ height: 20px;
+ line-height: 20px;
+ color: #505050;
+ text-shadow: 0px 1px 1px #fff;
+ border: 1px solid #e6e6e6;
+ background: #d8d8d8;
+ background: -moz-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d8d8d8), color-stop(100%,#bababa));
+ background: -o-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
+ background: -ms-linear-gradient(top, #d8d8d8 0%, #bababa 100%);
+ background: linear-gradient(top, #d8d8d8 0%, #bababa 100%);
+ box-shadow: 0 1px 1px 0 #999;
+ -o-box-shadow: 0 1px 1px 0 #999;
+ -webkit-box-shadow: 0 1px 1px 0 #999;
+ -moz-box-shadow: 0 1px 1px 0 #999;
+ text-decoration: none;
+}
+
+.calendarmain #calendar .fc-button.fc-state-disabled {
+ color: #999;
+ background: #d8d8d8;
+}
+
+.calendarmain .fc-button.fc-state-active,
+.calendarmain .fc-button.fc-state-down,
+.calendarmain #calendar .fc-button.fc-state-active,
+.calendarmain #calendar .fc-button.fc-state-down {
+ color: #333;
+ background: #bababa;
+ background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
+ background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
+ background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
+ background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
+}
+
+.calendarmain #calendar .fc-header .fc-button {
+ margin-left: -1px;
+ margin-right: 0;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button {
+ display: inline-block;
+ margin: 0;
+ text-align: center;
+ font-size: 10px;
+ color: #555;
+ min-width: 50px;
+ max-width: 75px;
+ height: 13px;
+ line-height: 1em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin: -7px 0 0 0;
+ padding: 28px 2px 0 2px;
+ text-shadow: 0px 1px 1px #EEE;
+ border: 0;
+ background: url(images/toolbar.png) center 100px no-repeat;
+ box-shadow: none;
+ -o-box-shadow: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ outline: none;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button:focus {
+ color: #fff;
+ text-shadow: 0px 1px 1px #666;
+ background-color: rgba(30,150,192, 0.5);
+ border-radius: 3px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button.fc-state-active {
+ font-weight: bold;
+ color: #222;
+ text-shadow: none;
+ background-color: transparent;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-agendaDay {
+ background-position: center -120px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-agendaDay.fc-state-active {
+ background-position: center -160px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-agendaWeek {
+ background-position: center -200px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-agendaWeek.fc-state-active {
+ background-position: center -240px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-month {
+ background-position: center -280px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-month.fc-state-active {
+ background-position: center -320px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-table {
+ background-position: center -360px;
+}
+
+.calendarmain #calendar .fc-header-left .fc-button-table.fc-state-active {
+ background-position: center -400px;
+}
+
+.calendarmain #calendar .fc-header-right {
+ padding-right: 252px;
+ padding-top: 4px;
+}
+
+.calendarmain #calendar .fc-header-title {
+ padding-top: 5px;
+}
+
+.fc-event {
+ font-size: 1em !important;
+}
+
+.fc-event-hori.fc-type-freebusy,
+.fc-event-vert.fc-type-freebusy {
+ opacity: 0.60;
+/*
+ color: #fff !important;
+ background: rgba(80,80,80,0.85) !important;
+ background: -moz-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.9) 100%) !important;
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(80,80,80,0.85)), color-stop(100%,rgba(48,48,48,0.9))) !important;
+ background: -webkit-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
+ background: -o-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
+ background: -ms-linear-gradient(top, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
+ background: linear-gradient(to bottom, rgba(80,80,80,0.85) 0%, rgba(48,48,48,0.85) 100%) !important;
+ border-color: #444 !important;
+ cursor: default !important;
+*/
+ -moz-box-shadow: inset 0px 1px 0 0px #888;
+ -webkit-box-shadow: inset 0px 1px 0 0px #888;
+ -o-box-shadow: inset 0px 1px 0 0px #888;
+ box-shadow: inset 0px 1px 0 0px #888;
+}
+
+.fc-event-row.fc-type-freebusy td {
+ color: #999;
+}
+
+.fc-event-hori.fc-type-freebusy .fc-event-skin,
+.fc-event-hori.fc-type-freebusy .fc-event-inner,
+.fc-event-vert.fc-type-freebusy .fc-event-skin,
+.fc-event-vert.fc-type-freebusy .fc-event-inner {
+/*
+ background-color: transparent !important;
+ border-color: #444 !important;
+ color: #fff !important;
+ text-shadow: 0 1px 1px #000;
+*/
+}
+
+.fc-event-hori.fc-type-freebusy .fc-event-title,
+.fc-event-vert.fc-type-freebusy .fc-event-title {
+ position: absolute;
+ top: -5000px;
+}
+
+.fc-event-vert.fc-invitation-needs-action,
+.fc-event-hori.fc-invitation-needs-action {
+ border: 1px dashed #5757c7 !important;
+}
+
+.fc-event-vert.fc-invitation-tentative,
+.fc-event-hori.fc-invitation-tentative {
+ border: 1px dashed #eb8900 !important;
+}
+
+.fc-event-vert.fc-invitation-declined,
+.fc-event-hori.fc-invitation-declined {
+ border: 1px dashed #c00 !important;
+}
+
+.fc-event-vert.fc-event-ns-other.fc-invitation-declined,
+.fc-event-hori.fc-event-ns-other.fc-invitation-declined {
+ opacity: 0.7;
+}
+
+.fc-event-ns-other.fc-invitation-declined .fc-event-title {
+ text-decoration: line-through;
+}
+
+.fc-event-vert.fc-invitation-tentative .fc-event-head,
+.fc-event-vert.fc-invitation-declined .fc-event-head,
+.fc-event-vert.fc-invitation-needs-action .fc-event-head {
+/* background-color: transparent !important; */
+}
+
+.fc-event-vert.fc-invitation-tentative .fc-event-bg {
+ background: url(data:image/gif;base64,R0lGODlhCAAIAPABAOuJAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-event-vert.fc-invitation-needs-action .fc-event-bg {
+ background: url(data:image/gif;base64,R0lGODlhCAAIAPABAFdXx////yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-event-vert.fc-invitation-declined .fc-event-bg {
+ background: url(data:image/gif;base64,R0lGODlhCAAIAPABAMwAAP///yH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAQAsAAAAAAgACAAAAg4Egmipx+ZaDPCtVPFNBQA7) 0 0 repeat #fff;
+}
+
+.fc-view-table tr.fc-invitation-tentative td,
+.fc-view-table tr.fc-invitation-declined td,
+.fc-view-table tr.fc-invitation-needs-action td {
+ color: #888;
+}
+
+.fc-view-table tr.fc-invitation-tentative td.fc-event-title,
+.fc-view-table tr.fc-invitation-declined td.fc-event-title,
+.fc-view-table tr.fc-invitation-needs-action td.fc-event-title {
+ font-weight: normal;
+}
+
+#quickview-calendar .fc-view-table tr.fc-invitation-tentative td,
+#quickview-calendar .fc-view-table tr.fc-invitation-declined td,
+#quickview-calendar .fc-view-table tr.fc-invitation-needs-action td {
+ color: #333;
+}
+
+.calendarmain .fc-event:focus {
+ outline: 1px solid rgba(71,135,177, 0.4);
+ -webkit-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+ -moz-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+ -o-box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+ box-shadow: 0 0 2px 3px rgba(71,135,177, 0.6);
+}
+.fc-event-title {
+ font-weight: bold;
+}
+
+.cal-event-status-cancelled .fc-event-title {
+ text-decoration: line-through;
+}
+
+.fc-event-hori .fc-event-title {
+ font-weight: normal;
+ white-space: nowrap;
+}
+
+.fc-event-hori .fc-event-time {
+ white-space: nowrap;
+ font-weight: normal !important;
+ font-size: 10px;
+ padding-right: 0.6em;
+}
+
+.fc-grid .fc-event-time {
+ font-weight: normal !important;
+ padding-right: 0.3em;
+}
+
+.calendarmain .fc-event-vert .fc-event-inner {
+ z-index: 0;
+}
+
+.fc-event-cateories {
+ font-style:italic;
+}
+
+div.fc-event-location {
+ font-size: 90%;
+}
+
+.fc-more-link {
+ color: #999;
+ padding-top: 1px;
+ cursor: pointer;
+}
+
+.fc-agenda-slots td div {
+ height: 22px;
+}
+
+.fc-sat, .fc-sun {
+ background-color: rgba(198,198,198, 0.08);
+}
+
+.calendarmain .fc-state-highlight {
+ background-color: rgba(233,198,14, 0.12);
+}
+
+.fc-widget-header {
+ background-color: #d6eaf3;
+ color: #004458;
+ text-shadow: 0px 1px 1px #fff;
+}
+
+.fc-view thead th.fc-widget-header {
+ padding: 8px 0;
+ color: #69939e;
+}
+
+.fc-day-number {
+ color: #578da5;
+}
+
+.fc-icon-alarms,
+.fc-icon-sensitive,
+.fc-icon-recurring {
+ display: inline-block;
+ width: 11px;
+ height: 11px;
+ background: url(images/eventicons.png) 0 0 no-repeat;
+ margin-left: 3px;
+ line-height: 10px;
+}
+
+.fc-icon-alarms {
+ background-position: 0 -13px;
+}
+
+.fc-icon-sensitive {
+ background-position: 0 -25px;
+}
+
+.fc-list-section .fc-event {
+ cursor: pointer;
+}
+
+.calendarmain .fc-view-table td.fc-list-header {
+ color: #004458;
+ font-size: 12px;
+}
+
+.calendarmain .fc-view-table tr.fc-event td {
+ border-color: #ddd;
+ padding: 4px 7px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.calendarmain .fc-view-table tr.fc-event td.fc-event-handle {
+ padding: 5px 0 2px 7px;
+ width: 12px;
+}
+
+.calendarmain .fc-view-table .fc-event-handle .fc-event-skin {
+ margin: 0;
+ padding: 0;
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ font-size: 6px;
+ border-radius: 8px;
+}
+
+.calendarmain .fc-view-table .fc-event-handle .fc-event-inner {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ padding: 0;
+ margin: -1px;
+ font-size: 10px;
+ border-radius: 8px;
+ border: 1px solid rgba(0, 0, 0, 0.4);
+ -webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
+ -moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
+ box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
+}
+
+.calendarmain .fc-view-table col.fc-event-location {
+ width: 25%;
+}
+
+.fc-view-table table.fc-list-smart {
+/* table-layout: auto; */
+}
+
+.fc-listappend {
+ text-align: center;
+ margin: 1em 0;
+}
+
+.fc-listappend .message {
+ padding: 0.5em;
+ margin-bottom: 0.5em;
+ font-size: 150%;
+ color: #999;
+}
+
+.fc-listappend .formlinks a {
+ font-size: 12px;
+ padding: 0 0.3em;
+}
+
+.fc-event-temp {
+ opacity: 0.4;
+ filter: alpha(opacity=40); /* IE8 */
+}
+
+/* Settings section */
+
+fieldset #calendarcategories div {
+ margin-bottom: 0.3em;
+}
+
+/* Invitation UI in mail */
+
+.messagelist tbody .attachment span.ical {
+ display: inline-block;
+ vertical-align: middle;
+ height: 18px;
+ width: 20px;
+ padding: 0;
+ background: url(images/ical-attachment.png) 2px 1px no-repeat;
+}
+
+ul.toolbarmenu li a.calendarlink span.calendar,
+#attachmentmenu li a.calendarlink span.calendar {
+ background-position: 0px -2197px;
+}
+
+div.calendar-invitebox {
+ min-height: 20px;
+ margin: 5px 8px;
+ padding: 3px 6px 6px 34px;
+ border: 1px solid #ffdf0e;
+ background: url(images/calendar.png) 6px 5px no-repeat #fef893;
+}
+
+div.calendar-invitebox td.ititle {
+ font-weight: bold;
+ padding-right: 0.5em;
+}
+
+div.calendar-invitebox td {
+ padding: 2px;
+}
+
+div.calendar-invitebox td.label {
+ color: #666;
+ padding-right: 1em;
+}
+
+div.calendar-invitebox td.sensitivity {
+ color: #d31400;
+ font-weight: bold;
+}
+
+div.calendar-invitebox td.recurrence-id {
+ text-transform: uppercase;
+ font-style: italic;
+}
+
+div.calendar-invitebox td em {
+ font-weight: bold;
+}
+
+#event-rsvp .rsvp-buttons,
+div.calendar-invitebox .itip-buttons div {
+ margin-top: 0.5em;
+}
+
+#event-rsvp input.button,
+div.calendar-invitebox input.button {
+ font-weight: bold;
+ margin-right: 0.5em;
+}
+
+div.calendar-invitebox input.button.preview {
+ margin-left: 1em;
+ margin-right: 0;
+}
+
+div.calendar-invitebox .folder-select {
+ font-weight: 10px;
+ margin-left: 1em;
+ white-space: nowrap;
+}
+
+div.calendar-invitebox .rsvp-status {
+ padding-left: 2px;
+}
+
+div.calendar-invitebox .rsvp-status.loading {
+ color: #666;
+ padding: 1px 0 2px 24px;
+ background: url(images/loading_blue.gif) top left no-repeat;
+}
+
+div.calendar-invitebox .rsvp-status.hint {
+ color: #666;
+ text-shadow: none;
+ font-style: italic;
+}
+
+#event-partstat .changersvp,
+div.calendar-invitebox .rsvp-status.declined,
+div.calendar-invitebox .rsvp-status.tentative,
+div.calendar-invitebox .rsvp-status.accepted,
+div.calendar-invitebox .rsvp-status.delegated,
+div.calendar-invitebox .rsvp-status.needs-action {
+ padding: 0 0 1px 22px;
+ background: url(images/attendee-status.png) 2px -20px no-repeat;
+}
+
+#event-partstat .changersvp.declined,
+div.calendar-invitebox .rsvp-status.declined {
+ background-position: 2px -40px;
+}
+
+#event-partstat .changersvp.tentative,
+div.calendar-invitebox .rsvp-status.tentative {
+ background-position: 2px -60px;
+}
+
+#event-partstat .changersvp.delegated,
+div.calendar-invitebox .rsvp-status.delegated {
+ background-position: 2px -180px;
+}
+
+#event-partstat .changersvp.needs-action,
+div.calendar-invitebox .rsvp-status.needs-action {
+ background-position: 2px 0;
+}
+
+div.calendar-invitebox .calendar-agenda-preview {
+ display: none;
+ border-top: 1px solid #dfdfdf;
+ margin-top: 1em;
+ padding-top: 0.6em;
+}
+
+div.calendar-invitebox .calendar-agenda-preview h3.preview-title {
+ margin: 0 0 0.5em 0;
+ font-size: 12px;
+ color: #333;
+}
+
+div.calendar-invitebox .calendar-agenda-preview .event-row {
+ color: #777;
+ padding: 2px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+div.calendar-invitebox .calendar-agenda-preview .event-row.current {
+ color: #333;
+ font-weight: bold;
+}
+
+div.calendar-invitebox .calendar-agenda-preview .event-row.no-event {
+ font-style: italic;
+}
+
+div.calendar-invitebox .calendar-agenda-preview .event-date {
+ display: inline-block;
+ min-width: 8em;
+ margin-right: 1em;
+ white-space: nowrap;
+}
+
+
+/* iTIP attend reply page */
+
+.calendaritipattend .centerbox {
+ width: 40em;
+ min-height: 7em;
+ margin: 80px auto 0 auto;
+ padding: 10px 10px 10px 90px;
+ background: url(images/invitation.png) 10px 10px no-repeat #fff;
+}
+
+.calendaritipattend #message {
+ width: 46em;
+ margin: 0 auto;
+ padding: 10px;
+}
+
+.calendaritipattend .calendar-invitebox {
+ background: none;
+ padding-left: 0;
+ border: 0;
+ margin: 0 0 2em 0;
+}
+
+.calendaritipattend .calendar-invitebox .rsvp-status {
+ margin-top: 2.5em;
+ font-size: 110%;
+ font-weight: bold;
+}
+
+.calendaritipattend .calendar-invitebox td.title,
+.calendaritipattend .calendar-invitebox td.ititle {
+ font-size: 120%;
+}
+
+.calendaritipattend .itip-reply-controls .noreply-toggle,
+.calendaritipattend .itip-reply-controls #noreply-event-rsvp {
+ display: none;
+}
+
+.calendaritipattend .itip-reply-controls a.reply-comment-toggle {
+ margin-left: 2px;
+}
+
diff --git a/calendar/skins/larry/fullcalendar.css b/calendar/skins/larry/fullcalendar.css
new file mode 100644
index 0000000..daefdf2
--- /dev/null
+++ b/calendar/skins/larry/fullcalendar.css
@@ -0,0 +1,711 @@
+/*!
+ * FullCalendar v1.6.4-rcube-1.0 Stylesheet
+ * Docs & License: http://arshaw.com/fullcalendar/
+ * (c) 2013 Adam Shaw, 2014 Kolab Systems AG
+ */
+
+
+.fc {
+ direction: ltr;
+ text-align: left;
+ }
+
+.fc table {
+ border-collapse: collapse;
+ border-spacing: 0;
+ }
+
+html .fc,
+.fc table {
+ font-size: 1em;
+ }
+
+.fc td,
+.fc th {
+ padding: 0;
+ vertical-align: top;
+ }
+
+
+
+/* Header
+------------------------------------------------------------------------*/
+
+.fc-header td {
+ white-space: nowrap;
+ }
+
+.fc-header-left {
+ width: 25%;
+ text-align: left;
+ }
+
+.fc-header-center {
+ text-align: center;
+ }
+
+.fc-header-right {
+ width: 25%;
+ text-align: right;
+ }
+
+.fc-header-title {
+ display: inline-block;
+ vertical-align: top;
+ }
+
+.fc-header-title h2 {
+ margin-top: 0;
+ white-space: nowrap;
+ }
+
+.fc .fc-header-space {
+ padding-left: 10px;
+ }
+
+.fc-header .fc-button {
+ margin-bottom: 1em;
+ vertical-align: top;
+ }
+
+/* buttons edges butting together */
+
+.fc-header .fc-button {
+ margin-right: -1px;
+ }
+
+.fc-header .fc-corner-right, /* non-theme */
+.fc-header .ui-corner-right { /* theme */
+ margin-right: 0; /* back to normal */
+ }
+
+/* button layering (for border precedence) */
+
+.fc-header .fc-state-hover,
+.fc-header .ui-state-hover {
+ z-index: 2;
+ }
+
+.fc-header .fc-state-down {
+ z-index: 3;
+ }
+
+.fc-header .fc-state-active,
+.fc-header .ui-state-active {
+ z-index: 4;
+ }
+
+
+
+/* Content
+------------------------------------------------------------------------*/
+
+.fc-content {
+ clear: both;
+ zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */
+ }
+
+.fc-view {
+ width: 100%;
+ overflow: hidden;
+ }
+
+
+
+/* Cell Styles
+------------------------------------------------------------------------*/
+
+.fc-widget-header, /* <th>, usually */
+.fc-widget-content { /* <td>, usually */
+ border: 1px solid #ddd;
+ }
+
+.fc-state-highlight { /* <td> today cell */ /* TODO: add .fc-today to <th> */
+ background: #fcf8e3;
+ }
+
+.fc-cell-overlay { /* semi-transparent rectangle while dragging */
+ background: #bce8f1;
+ opacity: .3;
+ filter: alpha(opacity=30); /* for IE */
+ }
+
+
+
+/* Buttons
+------------------------------------------------------------------------*/
+
+.fc-button {
+ position: relative;
+ display: inline-block;
+ padding: 0 .6em;
+ overflow: hidden;
+ height: 1.9em;
+ line-height: 1.9em;
+ white-space: nowrap;
+ cursor: pointer;
+ }
+
+.fc-state-default { /* non-theme */
+ border: 1px solid;
+ }
+
+.fc-state-default.fc-corner-left { /* non-theme */
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+
+.fc-state-default.fc-corner-right { /* non-theme */
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+
+/*
+ Our default prev/next buttons use HTML entities like &lsaquo; &rsaquo; &laquo; &raquo;
+ and we'll try to make them look good cross-browser.
+*/
+
+.fc-text-arrow {
+ margin: 0 .1em;
+ font-size: 2em;
+ font-family: "Courier New", Courier, monospace;
+ vertical-align: baseline; /* for IE7 */
+ }
+
+.fc-button-prev .fc-text-arrow,
+.fc-button-next .fc-text-arrow { /* for &lsaquo; &rsaquo; */
+ font-weight: bold;
+ }
+
+/* icon (for jquery ui) */
+
+.fc-button .fc-icon-wrap {
+ position: relative;
+ float: left;
+ top: 50%;
+ }
+
+.fc-button .ui-icon {
+ position: relative;
+ float: left;
+ margin-top: -50%;
+ *margin-top: 0;
+ *top: -50%;
+ }
+
+/*
+ button states
+ borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/)
+*/
+
+.fc-state-default {
+ background-color: #f5f5f5;
+ background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
+ background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
+ background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
+ background-repeat: repeat-x;
+ border-color: #e6e6e6 #e6e6e6 #bfbfbf;
+ border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
+ color: #333;
+ text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
+ }
+
+.fc-state-hover,
+.fc-state-down,
+.fc-state-active,
+.fc-state-disabled {
+ color: #333333;
+ background-color: #e6e6e6;
+ }
+
+.fc-state-hover {
+ color: #333333;
+ text-decoration: none;
+ background-position: 0 -15px;
+ -webkit-transition: background-position 0.1s linear;
+ -moz-transition: background-position 0.1s linear;
+ -o-transition: background-position 0.1s linear;
+ transition: background-position 0.1s linear;
+ }
+
+.fc-state-down,
+.fc-state-active {
+ background-color: #cccccc;
+ background-image: none;
+ outline: 0;
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
+ }
+
+.fc-state-disabled {
+ cursor: default;
+ background-image: none;
+ opacity: 0.65;
+ filter: alpha(opacity=65);
+ box-shadow: none;
+ }
+
+
+
+/* Global Event Styles
+------------------------------------------------------------------------*/
+
+.fc-event-container > * {
+ z-index: 8;
+ }
+
+.fc-event-container > .ui-draggable-dragging,
+.fc-event-container > .ui-resizable-resizing {
+ z-index: 9;
+ }
+
+.fc-event {
+ border: 1px solid #3a87ad; /* default BORDER color */
+ background-color: #3a87ad; /* default BACKGROUND color */
+ color: #fff; /* default TEXT color */
+ font-size: .85em;
+ cursor: default;
+ }
+
+.fc-event:focus {
+ outline: 2px solid ActiveBorder;
+ }
+
+a.fc-event {
+ text-decoration: none;
+ }
+
+a.fc-event,
+.fc-event-draggable {
+ cursor: pointer;
+ }
+
+.fc-rtl .fc-event {
+ text-align: right;
+ }
+
+.fc-event-inner {
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ }
+
+.fc-event-time,
+.fc-event-title {
+ padding: 0 1px;
+ }
+
+.fc .ui-resizable-handle {
+ display: block;
+ position: absolute;
+ z-index: 99999;
+ overflow: hidden; /* hacky spaces (IE6/7) */
+ font-size: 300%; /* */
+ line-height: 50%; /* */
+ }
+
+
+
+/* Horizontal Events
+------------------------------------------------------------------------*/
+
+.fc-event-hori {
+ border-width: 1px 0;
+ margin-bottom: 1px;
+ }
+
+.fc-ltr .fc-event-hori.fc-event-start,
+.fc-rtl .fc-event-hori.fc-event-end {
+ border-left-width: 1px;
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+ }
+
+.fc-ltr .fc-event-hori.fc-event-end,
+.fc-rtl .fc-event-hori.fc-event-start {
+ border-right-width: 1px;
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ }
+
+/* resizable */
+
+.fc-event-hori .ui-resizable-e {
+ top: 0 !important; /* importants override pre jquery ui 1.7 styles */
+ right: -3px !important;
+ width: 7px !important;
+ height: 100% !important;
+ cursor: e-resize;
+ }
+
+.fc-event-hori .ui-resizable-w {
+ top: 0 !important;
+ left: -3px !important;
+ width: 7px !important;
+ height: 100% !important;
+ cursor: w-resize;
+ }
+
+.fc-event-hori .ui-resizable-handle {
+ _padding-bottom: 14px; /* IE6 had 0 height */
+ }
+
+
+
+/* Reusable Separate-border Table
+------------------------------------------------------------*/
+
+table.fc-border-separate {
+ border-collapse: separate;
+ }
+
+.fc-border-separate th,
+.fc-border-separate td {
+ border-width: 1px 0 0 1px;
+ }
+
+.fc-border-separate th.fc-last,
+.fc-border-separate td.fc-last {
+ border-right-width: 1px;
+ }
+
+.fc-border-separate tr.fc-last th,
+.fc-border-separate tr.fc-last td {
+ border-bottom-width: 1px;
+ }
+
+.fc-border-separate tbody tr.fc-first td,
+.fc-border-separate tbody tr.fc-first th {
+ border-top-width: 0;
+ }
+
+
+
+/* Month View, Basic Week View, Basic Day View
+------------------------------------------------------------------------*/
+
+.fc-grid th {
+ text-align: center;
+ }
+
+.fc .fc-week-number {
+ width: 22px;
+ text-align: center;
+ }
+
+.fc .fc-week-number div {
+ padding: 0 2px;
+ }
+
+.fc-grid .fc-day-number {
+ float: right;
+ padding: 0 2px;
+ }
+
+.fc-grid .fc-other-month .fc-day-number {
+ opacity: 0.3;
+ filter: alpha(opacity=30); /* for IE */
+ /* opacity with small font can sometimes look too faded
+ might want to set the 'color' property instead
+ making day-numbers bold also fixes the problem */
+ }
+
+.fc-grid .fc-day-content {
+ clear: both;
+ padding: 2px 2px 1px; /* distance between events and day edges */
+ }
+
+/* event styles */
+
+.fc-grid .fc-event-time {
+ font-weight: bold;
+ }
+
+/* right-to-left */
+
+.fc-rtl .fc-grid .fc-day-number {
+ float: left;
+ }
+
+.fc-rtl .fc-grid .fc-event-time {
+ float: right;
+ }
+
+.fc-more-link {
+ font-size: 0.85em;
+ white-space: nowrap;
+ text-decoration: none;
+ cursor: pointer;
+ padding: 1px;
+}
+
+/* Agenda Week View, Agenda Day View
+------------------------------------------------------------------------*/
+
+.fc-agenda table {
+ border-collapse: separate;
+ }
+
+.fc-agenda-days th {
+ text-align: center;
+ }
+
+.fc-agenda .fc-agenda-axis {
+ width: 50px;
+ padding: 0 4px;
+ vertical-align: middle;
+ text-align: right;
+ white-space: nowrap;
+ font-weight: normal;
+ }
+
+.fc-agenda .fc-week-number {
+ font-weight: bold;
+ }
+
+.fc-agenda .fc-day-content {
+ padding: 2px 2px 1px;
+ }
+
+/* make axis border take precedence */
+
+.fc-agenda-days .fc-agenda-axis {
+ border-right-width: 1px;
+ }
+
+.fc-agenda-days .fc-col0 {
+ border-left-width: 0;
+ }
+
+/* all-day area */
+
+.fc-agenda-allday th {
+ border-width: 0 1px;
+ }
+
+.fc-agenda-allday .fc-day-content {
+ min-height: 34px; /* TODO: doesnt work well in quirksmode */
+ _height: 34px;
+ }
+
+/* divider (between all-day and slots) */
+
+.fc-agenda-divider-inner {
+ height: 2px;
+ overflow: hidden;
+ }
+
+.fc-widget-header .fc-agenda-divider-inner {
+ background: #eee;
+ }
+
+/* slot rows */
+
+.fc-agenda-slots th {
+ border-width: 1px 1px 0;
+ }
+
+.fc-agenda-slots td {
+ border-width: 1px 0 0;
+ background: none;
+ }
+
+.fc-agenda-slots td div {
+ height: 20px;
+ }
+
+.fc-agenda-slots tr.fc-slot0 th,
+.fc-agenda-slots tr.fc-slot0 td {
+ border-top-width: 0;
+ }
+
+.fc-agenda-slots tr.fc-minor th,
+.fc-agenda-slots tr.fc-minor td {
+ border-top-style: dotted;
+ }
+
+.fc-agenda-slots tr.fc-minor th.ui-widget-header {
+ *border-top-style: solid; /* doesn't work with background in IE6/7 */
+ }
+
+
+
+/* Vertical Events
+------------------------------------------------------------------------*/
+
+.fc-event-vert {
+ border-width: 0 1px;
+ }
+
+.fc-event-vert .fc-event-head,
+.fc-event-vert .fc-event-content {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ overflow: hidden;
+ }
+
+.fc-event-vert.fc-event-start {
+ border-top-width: 1px;
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ }
+
+.fc-event-vert.fc-event-end {
+ border-bottom-width: 1px;
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+ }
+
+.fc-event-vert .fc-event-time {
+ white-space: nowrap;
+ font-size: 10px;
+ }
+
+.fc-event-vert .fc-event-inner {
+ position: relative;
+ z-index: 2;
+ }
+
+.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */
+ position: absolute;
+ z-index: 1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #fff;
+ opacity: .25;
+ filter: alpha(opacity=25);
+ }
+
+.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */
+.fc-select-helper .fc-event-bg {
+ display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */
+ }
+
+/* resizable */
+
+.fc-event-vert .ui-resizable-s {
+ bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */
+ width: 100% !important;
+ height: 8px !important;
+ overflow: hidden !important;
+ line-height: 8px !important;
+ font-size: 11px !important;
+ font-family: monospace;
+ text-align: center;
+ cursor: s-resize;
+ }
+
+.fc-agenda .ui-resizable-resizing { /* TODO: better selector */
+ _overflow: hidden;
+ }
+
+.fc-timeline {
+ position: absolute;
+ width: 100%;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ border: none;
+ border-top: 2px solid #3ec400;
+ z-index: 999;
+}
+
+/* List view (by bruederli@kolabsys.com)
+------------------------------------------------------------------------*/
+
+.fc-view-list,
+.fc-view-table {
+ border: 1px solid #ccc;
+ width: auto;
+}
+
+.fc-view-list .fc-list-header,
+.fc-view-table td.fc-list-header {
+ border-width: 0;
+ border-bottom-width: 1px;
+ padding: 3px 5px;
+}
+
+.fc-view-table .fc-first td.fc-list-header {
+ border-top-width: 0;
+}
+
+.fc-list-section {
+ padding: 4px 2px;
+ border-width: 0;
+ border-bottom-width: 1px;
+}
+
+.fc-view-list .fc-last {
+ border-bottom-width: 0;
+}
+
+.fc-list-section .fc-event {
+ position: relative;
+ margin: 1px 2px 3px 2px;
+}
+
+.fc-view-table tr.fc-event {
+ background: inherit;
+ color: inherit;
+}
+
+.fc-view-table tr.fc-event td {
+ padding: 2px;
+ border-bottom: 1px solid #ccc;
+}
+
+.fc-view-table tr.fc-event td.fc-event-handle {
+ padding: 3px 8px 3px 3px;
+}
+
+.fc-view-table .fc-event-handle .fc-event-skin {
+ border-radius: 2px;
+ -moz-border-radius: 2px;
+}
+
+.fc-view-table .fc-event-handle .fc-event-inner {
+ display: block;
+ width: 8px;
+ height: 10px;
+ border-radius: 2px;
+ -moz-border-radius: 2px;
+}
+
+.fc-view-table table {
+ table-layout: fixed;
+ width: 100%;
+}
+
+.fc-view-table col.fc-event-handle {
+ width: 18px;
+}
+
+.fc-event-handle .fc-event-inner {
+ border-color: inherit;
+ background-color: inherit;
+}
+
+.fc-view-table col.fc-event-date {
+ width: 7em;
+}
+
+.fc-view-table .fc-list-day col.fc-event-date {
+ width: 1px;
+}
+
+.fc-view-table col.fc-event-time {
+ width: 9em;
+}
+
+.fc-view-table td.fc-event-date,
+.fc-view-table td.fc-event-time {
+ white-space: nowrap;
+ padding-right: 1em;
+}
+
diff --git a/calendar/skins/larry/iehacks.css b/calendar/skins/larry/iehacks.css
new file mode 100644
index 0000000..d6e122d
--- /dev/null
+++ b/calendar/skins/larry/iehacks.css
@@ -0,0 +1,77 @@
+/* CSS hacks for IE 7 */
+
+#calendarsidebar,
+#calendarsidebartoggle {
+ height: expression((parseInt(this.parentNode.offsetHeight)-37)+'px');
+}
+
+#calendar {
+ width: expression((parseInt(this.parentNode.offsetWidth)-parseInt(document.getElementById('calendarsidebartoggle').offsetWidth)-parseInt(document.getElementById('calendarsidebartoggle').offsetLeft)-4)+'px');
+ height: expression((parseInt(this.parentNode.offsetHeight)-30)+'px');
+}
+
+#calendars {
+ height: expression((parseInt(this.parentNode.offsetHeight)-240)+'px');
+}
+
+#agendaoptions {
+ width: expression((parseInt(this.parentNode.offsetWidth)-12)+'px');
+}
+
+#calendartoolbar a.buttonPas {
+ filter: alpha(opacity=35);
+}
+
+#datepicker a.ui-priority-secondary {
+ filter: alpha(opacity=40);
+}
+
+.calendarmain .fc-day-content {
+ cursor: default;
+}
+
+.calendarmain .fc-view-table col.fc-event-date {
+ width: 8em;
+}
+
+.calendarmain .fc-view-table col.fc-event-time {
+ width: 9em;
+}
+
+.calendarmain .fc-header-title h2 {
+ font-size: 16px;
+}
+
+.calendarmain .fc-header-left {
+ width: 248px;
+}
+
+.calendarmain .fc-header-center {
+ width: auto;
+}
+
+.calendarmain .fc-header-right {
+ width: 144px;
+ white-space: nowrap;
+}
+
+.calendarmain .fc-event-temp .fc-event-bg {
+ display: none; /* nested opacity filters while dragging don't work */
+}
+
+#schedule-event-time {
+ filter: alpha(opacity=40);
+}
+
+#eventfreebusy .schedule-buttons,
+#edit-attendees-form #edit-attendee-schedule {
+ right: 0.6em;
+}
+
+#schedule-freebusy-times tr.times td.allday {
+ width: expression(Math.max(60, parseInt(this.offsetWidth))+'px');
+}
+
+.ui-dialog .ui-dialog-titlebar {
+ width: expression((parseInt(this.parentNode.offsetWidth)-26)+'px');
+}
diff --git a/calendar/skins/larry/images/attendee-status.png b/calendar/skins/larry/images/attendee-status.png
new file mode 100644
index 0000000..37adcc1
--- /dev/null
+++ b/calendar/skins/larry/images/attendee-status.png
Binary files differ
diff --git a/calendar/skins/larry/images/autocomplete.png b/calendar/skins/larry/images/autocomplete.png
new file mode 100644
index 0000000..593c007
--- /dev/null
+++ b/calendar/skins/larry/images/autocomplete.png
Binary files differ
diff --git a/calendar/skins/larry/images/badge_cancelled.png b/calendar/skins/larry/images/badge_cancelled.png
new file mode 100644
index 0000000..2eb4878
--- /dev/null
+++ b/calendar/skins/larry/images/badge_cancelled.png
Binary files differ
diff --git a/calendar/skins/larry/images/badge_confidential.png b/calendar/skins/larry/images/badge_confidential.png
new file mode 100644
index 0000000..04a2052
--- /dev/null
+++ b/calendar/skins/larry/images/badge_confidential.png
Binary files differ
diff --git a/calendar/skins/larry/images/badge_private.png b/calendar/skins/larry/images/badge_private.png
new file mode 100644
index 0000000..52e4dbe
--- /dev/null
+++ b/calendar/skins/larry/images/badge_private.png
Binary files differ
diff --git a/calendar/skins/larry/images/calendar.png b/calendar/skins/larry/images/calendar.png
new file mode 100644
index 0000000..69d780d
--- /dev/null
+++ b/calendar/skins/larry/images/calendar.png
Binary files differ
diff --git a/calendar/skins/larry/images/calendars.png b/calendar/skins/larry/images/calendars.png
new file mode 100644
index 0000000..5b5ca85
--- /dev/null
+++ b/calendar/skins/larry/images/calendars.png
Binary files differ
diff --git a/calendar/skins/larry/images/eventicons.png b/calendar/skins/larry/images/eventicons.png
new file mode 100644
index 0000000..9be2c03
--- /dev/null
+++ b/calendar/skins/larry/images/eventicons.png
Binary files differ
diff --git a/calendar/skins/larry/images/focusview.png b/calendar/skins/larry/images/focusview.png
new file mode 100644
index 0000000..ce99400
--- /dev/null
+++ b/calendar/skins/larry/images/focusview.png
Binary files differ
diff --git a/calendar/skins/larry/images/freebusy-colors.png b/calendar/skins/larry/images/freebusy-colors.png
new file mode 100644
index 0000000..8d9257d
--- /dev/null
+++ b/calendar/skins/larry/images/freebusy-colors.png
Binary files differ
diff --git a/calendar/skins/larry/images/ical-attachment.png b/calendar/skins/larry/images/ical-attachment.png
new file mode 100644
index 0000000..653f4bc
--- /dev/null
+++ b/calendar/skins/larry/images/ical-attachment.png
Binary files differ
diff --git a/calendar/skins/larry/images/invitation.png b/calendar/skins/larry/images/invitation.png
new file mode 100644
index 0000000..af85804
--- /dev/null
+++ b/calendar/skins/larry/images/invitation.png
Binary files differ
diff --git a/calendar/skins/larry/images/loading_blue.gif b/calendar/skins/larry/images/loading_blue.gif
new file mode 100644
index 0000000..2ea6b19
--- /dev/null
+++ b/calendar/skins/larry/images/loading_blue.gif
Binary files differ
diff --git a/calendar/skins/larry/images/sendinvitation.png b/calendar/skins/larry/images/sendinvitation.png
new file mode 100644
index 0000000..dea30fa
--- /dev/null
+++ b/calendar/skins/larry/images/sendinvitation.png
Binary files differ
diff --git a/calendar/skins/larry/images/toggle.gif b/calendar/skins/larry/images/toggle.gif
new file mode 100644
index 0000000..d3be86f
--- /dev/null
+++ b/calendar/skins/larry/images/toggle.gif
Binary files differ
diff --git a/calendar/skins/larry/images/toolbar.png b/calendar/skins/larry/images/toolbar.png
new file mode 100644
index 0000000..5e79c66
--- /dev/null
+++ b/calendar/skins/larry/images/toolbar.png
Binary files differ
diff --git a/calendar/skins/larry/print.css b/calendar/skins/larry/print.css
new file mode 100644
index 0000000..fc5de97
--- /dev/null
+++ b/calendar/skins/larry/print.css
@@ -0,0 +1,229 @@
+/*** Printing styles for Calendar plugin ***/
+
+body {
+ margin: 0 0 1em 0;
+ color: #000;
+ background: #fff;
+}
+
+body, td, th, div, p, h3, select, input, textarea {
+ font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
+ font-size: 8pt;
+}
+
+#calendar {
+ position: relative;
+ top: 0;
+ left: 0;
+ height: auto;
+ margin: 5em auto 0 auto;
+ overflow: visible;
+}
+
+#calendar .fc-header-right {
+ padding-right: 0;
+}
+
+#printconfig {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ z-index: 10000;
+ padding: 0.5em;
+ background: #ebebeb;
+ border-bottom: 1px solid #999;
+ box-shadow: 0 3px 4px #ccc;
+ -moz-box-shadow: 0 3px 4px #ccc;
+ -webkit-box-shadow: 0 3px 4px #ccc;
+}
+
+#printconfig .prop {
+ padding-right: 2em;
+}
+
+#message {
+ position: absolute;
+ top: 5.5em;
+ left: 1em;
+}
+
+#message div.loading {
+ color: #666;
+ font-style: italic;
+}
+
+#calendarlist {
+ list-style: none;
+ margin: 2em 0;
+ padding-left: 1em;
+}
+
+#calendarlist ul {
+ float: left;
+ list-style: none;
+ padding-left: 0;
+}
+
+#calendarlist li {
+ float: left;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 0;
+ font-weight: bold;
+}
+
+#calendarlist li div {
+ float: left;
+ padding-right: 3em;
+ padding-bottom: 1em;
+}
+
+#calendarlist input,
+#calendarlist .handle {
+ display: none;
+}
+
+#calendarlist li.x-invitations div {
+ color: #999;
+ font-style: italic;
+}
+
+.calwidth {
+ width: 700px;
+ margin: 0 auto;
+}
+
+.rightalign {
+ float: right;
+ padding-top: 0.3em;
+}
+
+@media print {
+ .noprint,
+ .fc-header-right span {
+ display: none;
+ }
+
+ #calendar {
+ margin-top: 0;
+ }
+}
+
+/* fullcalendar style overrides */
+
+.fc-view {
+ overflow: visible;
+}
+
+.fc-event-skin,
+.fc-event-inner .fc-event-skin {
+ color: black;
+ background-color: #fff !important;
+}
+
+.fc-event-title {
+ font-weight: bold;
+}
+
+.fc-event-hori .fc-event-title {
+ font-weight: normal;
+ white-space: nowrap;
+}
+
+.fc-event-hori .fc-event-time {
+ white-space: nowrap;
+ font-weight: normal !important;
+ font-size: 10px;
+ padding-right: 0.6em;
+}
+
+.fc-grid .fc-event-time {
+ font-weight: normal !important;
+ padding-right: 0.3em;
+}
+
+.fc-event-cateories {
+ font-style: italic;
+}
+
+.fc-event-location {
+ font-size: 90%;
+}
+
+.fc-agenda-slots td div {
+ height: 1.4em;
+}
+
+.fc-widget-header,
+.fc-mon, .fc-tue, .fc-wed, .fc-thu, .fc-fri {
+ background-color: #fff;
+}
+
+.fc-widget-header, .fc-widget-content {
+ border-color: #ccc;
+}
+
+.fc-icon-alarms,
+.fc-icon-recurring {
+ display: inline-block;
+ width: 11px;
+ height: 11px;
+ background: url('images/eventicons.gif') 0 0 no-repeat;
+ margin-left: 3px;
+ line-height: 10px;
+}
+
+.fc-icon-alarms {
+ background-position: 0 -13px;
+}
+
+.fc-view-list, .fc-view-table {
+ border: 0;
+}
+
+.fc-view-list div.fc-list-header,
+.fc-view-table td.fc-list-header {
+ padding: 0.3em;
+ background: #fff;
+ font-weight: bold;
+ font-size: 1.2em;
+ color: #333;
+ border-color: #333;
+ border-style: solid;
+ border-width: 1px 0;
+ filter: none;
+}
+
+.fc-list-section .fc-event {
+ cursor: auto;
+}
+
+.fc-view-table tr.fc-event td,
+.fc-view-table tr.fc-event td.fc-event-handle {
+ border-color: #999;
+ padding-top: 0.5em;
+ padding-bottom: 0.5em;
+}
+
+.fc-view-table tr.fc-last td {
+ border: 0;
+}
+
+.fc-view-table tr.fc-event .fc-event-description {
+ padding-left: 2em;
+ padding-top: 0em;
+}
+
+.fc-event-vert .fc-event-description {
+ font-size: 90%;
+ font-style: italic;
+}
+
+.fc-view-month .fc-event-hori .fc-event-inner {
+ background: #fff !important;
+}
+
+.fc-view-table col.fc-event-location {
+ width: 20%;
+}
diff --git a/calendar/skins/larry/print.iehacks.css b/calendar/skins/larry/print.iehacks.css
new file mode 100644
index 0000000..5322ff9
--- /dev/null
+++ b/calendar/skins/larry/print.iehacks.css
@@ -0,0 +1,25 @@
+/* CSS hacks for IE 7 */
+
+#calendar {
+ top: 5em;
+}
+
+.calwidth {
+ width: 172mm;
+}
+
+.fc-header-title h2 {
+ font-size: 16px;
+}
+
+#calendarlist li {
+ float: none;
+ padding: 0;
+ margin-left: 1em;
+}
+
+@media print {
+ #calendar {
+ top: 0;
+ }
+} \ No newline at end of file
diff --git a/calendar/skins/larry/templates/attachment.html b/calendar/skins/larry/templates/attachment.html
new file mode 100644
index 0000000..17fccc5
--- /dev/null
+++ b/calendar/skins/larry/templates/attachment.html
@@ -0,0 +1,64 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body class="extwin calendar attachmentwin">
+
+<div id="header">
+<div id="topline" role="banner" aria-labelledby="aria-label-topnav">
+ <div class="topleft">
+ <roundcube:container name="topline-left" id="topline-left" />
+ </div>
+ <roundcube:container name="topline-center" id="topline-center" />
+ <div class="topright">
+ <roundcube:container name="topline-right" id="topline-right" />
+ <roundcube:button name="close" type="link" label="close" class="closelink" onclick="self.close()" />
+ </div>
+</div>
+</div>
+
+<div id="mainscreen">
+
+<h1 class="voice"><roundcube:label name="attachment" />: <roundcube:var name="env:filename" /></h1>
+
+<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
+<div id="attachmenttoolbar" class="toolbar fullwidth" role="toolbar" aria-labelledby="aria-label-toolbar">
+ <roundcube:button command="download-attachment" type="link" class="button download disabled" classAct="button download" classSel="button download pressed" label="download" title="download" />
+ <roundcube:button command="print-attachment" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" title="print" />
+ <roundcube:container name="toolbar" id="messagetoolbar" />
+</div>
+
+<div id="mainscreencontent">
+
+ <div id="partheader" class="uibox listbox" role="contentinfo" aria-labelledby="aria-label-contentinfo">
+ <h2 class="boxtitle" id="aria-label-contentinfo"><roundcube:label name="properties" /></h2>
+ <div class="scroller">
+ <roundcube:object name="plugin.attachmentcontrols" class="listing" />
+ </div>
+ </div>
+
+ <div id="attachmentcontainer" class="uibox" role="main" aria-labelledby="aria-label-messagepart">
+ <h2 id="aria-label-messagepart" class="voice"><roundcube:label name="arialabelattachmentpreview" /></h2>
+ <div class="iframebox">
+ <roundcube:object name="plugin.attachmentframe" id="attachmentframe" frameborder="0" title="arialabelattachmentpreview" />
+ </div>
+ </div>
+
+</div>
+
+</div>
+
+<script type="text/javascript">
+
+$(document).ready(function() {
+ if (window.rcube_splitter) {
+ new rcube_splitter({ id:'mailpartsplitterv', p1:'#partheader', p2:'#attachmentcontainer',
+ orientation:'v', relative:true, start:226, min:150, size:12}).init();
+ }
+});
+
+</script>
+</body>
+</html>
diff --git a/calendar/skins/larry/templates/calendar.html b/calendar/skins/larry/templates/calendar.html
new file mode 100644
index 0000000..e659ecc
--- /dev/null
+++ b/calendar/skins/larry/templates/calendar.html
@@ -0,0 +1,537 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="/this/iehacks.css" /><![endif]-->
+</head>
+<roundcube:if condition="env:extwin" /><body class="calendarmain extwin"><roundcube:else /><body class="calendarmain"><roundcube:endif />
+
+<roundcube:include file="/includes/header.html" />
+
+<h1 class="voice"><roundcube:label name="calendar.calendar" /></h1>
+
+<div id="mainscreen">
+ <div id="calendarsidebar">
+ <h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
+ <div id="calendartoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
+ <roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" />
+ <roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" />
+ <roundcube:button command="events-import" type="link" class="button import disabled" classAct="button import" classSel="button import pressed" label="import" title="calendar.importevents" />
+ <roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="calendar.export" title="calendar.exporttitle" />
+ <roundcube:container name="toolbar" id="calendartoolbar" />
+ </div>
+
+ <h2 id="aria-label-minical" class="voice"><roundcube:label name="calendar.arialabelminical" /></h2>
+ <div id="datepicker" class="uibox" role="presentation"></div>
+
+ <div id="calendars" class="uibox listbox" style="visibility:hidden" role="navigation" aria-labelledby="aria-label-calendarlist">
+ <h2 class="boxtitle" id="aria-label-calendarlist"><roundcube:label name="calendar.calendars" />
+ <a href="#calendars" class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />" tabindex="0"><roundcube:label name='calendar.findcalendars' /></a>
+ </h2>
+ <div class="listsearchbox">
+ <div class="searchbox" role="search" aria-labelledby="aria-label-calsearchform" aria-controls="calendarslist">
+ <h3 id="aria-label-calsearchform" class="voice"><roundcube:label name="calendar.arialabelcalsearchform" /></h3>
+ <label for="calendarlistsearch" class="voice"><roundcube:label name="calendar.searchterms" /></label>
+ <input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
+ <a class="iconbutton searchicon"></a>
+ <roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
+ </div>
+ </div>
+ <div class="scroller withfooter">
+ <roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
+ </div>
+ <div class="boxfooter">
+ <roundcube:button name="calendarcreatelink" id="calendarcreatemenulink" type="link" title="create" class="listbutton add" onclick="UI.show_popup('calendarcreatemenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
+ </div>
+ </div>
+ </div>
+
+ <div id="quicksearchbar" class="searchbox" role="search" aria-labelledby="aria-label-searchform">
+ <h2 id="aria-label-searchform" class="voice"><roundcube:label name="calendar.arialabelsearchform" /></h2>
+ <label for="quicksearchbox" class="voice"><roundcube:label name="calendar.arialabelquicksearchbox" /></label>
+ <roundcube:object name="plugin.searchform" id="quicksearchbox" />
+ <a id="searchmenulink" class="iconbutton searchoptions" tabindex="-1"> </a>
+ <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
+ </div>
+
+ <h2 id="aria-label-calendarview" class="voice"><roundcube:label name="calendar.arialabelcalendarview" /></h2>
+ <div id="calendar" role="main" aria-labelledby="aria-label-calendarview">
+ <roundcube:object name="plugin.angenda_options" class="boxfooter" id="agendaoptions" />
+ </div>
+</div>
+
+<div id="calendarcreatemenu" class="popupmenu">
+ <ul class="toolbarmenu">
+ <roundcube:object name="plugin.calendar_create_menu" />
+ </ul>
+</div>
+<div id="timezonedisplay"><roundcube:var name="env:timezone" /></div>
+
+<roundcube:object name="message" id="messagestack" />
+
+<div id="calendaroptionsmenu" class="popupmenu" aria-hidden="true">
+ <h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
+ <ul id="calendaroptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-calendaroptions">
+ <li role="menuitem"><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
+ <li role="menuitem"><roundcube:button command="calendar-delete" label="delete" classAct="active" /></li>
+ <roundcube:if condition="env:calendar_driver == 'kolab'" />
+ <li role="menuitem"><roundcube:button command="calendar-remove" label="calendar.removelist" classAct="active" /></li>
+ <roundcube:endif />
+ <li role="menuitem"><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
+ <roundcube:if condition="env:calendar_driver == 'kolab'" />
+ <li role="menuitem"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
+ <roundcube:endif />
+ </ul>
+</div>
+
+<div id="eventshow" class="uidialog eventdialog" aria-hidden="true">
+ <h1 id="event-title">Event Title</h1>
+ <div class="event-section" id="event-location">Location</div>
+ <div class="event-section" id="event-date">From-To</div>
+ <div class="event-section" id="event-description">
+ <h5 class="label"><roundcube:label name="calendar.description" /></h5>
+ <div class="event-text"></div>
+ </div>
+ <div class="event-section" id="event-url">
+ <h5 class="label"><roundcube:label name="calendar.url" /></h5>
+ <div class="event-text"></div>
+ </div>
+ <div class="event-section" id="event-repeat">
+ <h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
+ <div class="event-text"></div>
+ </div>
+ <div class="event-section" id="event-alarm">
+ <h5 class="label"><roundcube:label name="calendar.alarms" /></h5>
+ <div class="event-text"></div>
+ </div>
+ <div class="event-section event-attendees" id="event-attendees">
+ <h5 class="label"><roundcube:label name="calendar.tabattendees" /></h5>
+ <div class="event-text"></div>
+ </div>
+ <div class="event-line" id="event-partstat">
+ <label><roundcube:label name="calendar.mystatus" /></label>
+ <span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='calendar.changepartstat' />">
+ <span class="event-text"></span>
+ <a class="iconbutton edit"><roundcube:label name='calendar.changepartstat' /></a>
+ </span>
+ </div>
+ <div class="event-line" id="event-calendar">
+ <label><roundcube:label name="calendar.calendar" /></label>
+ <span class="event-text">Default</span>
+ </div>
+ <div class="event-line" id="event-category">
+ <label><roundcube:label name="calendar.category" /></label>
+ <span class="event-text"></span>
+ </div>
+ <div class="event-line" id="event-status">
+ <label><roundcube:label name="calendar.status" /></label>
+ <span class="event-text"></span>
+ </div>
+ <div class="event-line" id="event-free-busy">
+ <label><roundcube:label name="calendar.freebusy" /></label>
+ <span class="event-text"></span>
+ </div>
+ <div class="event-line" id="event-priority">
+ <label><roundcube:label name="calendar.priority" /></label>
+ <span class="event-text"></span>
+ </div>
+ <div class="event-line" id="event-sensitivity">
+ <label><roundcube:label name="calendar.sensitivity" /></label>
+ <span class="event-text"></span>
+ </div>
+ <div class="event-section" id="event-links">
+ <label><roundcube:label name="calendar.links" /></label>
+ <span class="event-text"></span>
+ <br style="clear:left">
+ </div>
+ <div class="event-section" id="event-attachments">
+ <label><roundcube:label name="attachments" /></label>
+ <div class="event-text"></div>
+ </div>
+ <div class="event-line" id="event-created-changed">
+ <label><roundcube:label name="calendar.created" /></label>
+ <span class="event-text event-created"></span>
+ <label><roundcube:label name="calendar.changed" /></label>
+ <span class="event-text event-changed"></span>
+ </div>
+ <div class="event-line" id="event-rsvp-comment">
+ <label><roundcube:label name="calendar.rsvpcomment" /></label>
+ <span class="event-text"></span>
+ </div>
+
+ <roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" class="event-dialog-message" style="display:none" />
+</div>
+
+<div id="eventoptionsmenu" class="popupmenu" aria-hidden="true">
+ <h3 id="aria-label-eventoptions" class="voice"><roundcube:label name="calendar.eventoptions" /></h3>
+ <ul id="eventoptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-eventoptions">
+ <li role="menuitem"><roundcube:button command="event-download" label="download" classAct="active" /></li>
+ <li role="menuitem"><roundcube:button command="event-sendbymail" label="send" classAct="active" /></li>
+ <roundcube:if condition="env:calendar_driver == 'kolab' && config:kolab_bonnie_api" />
+ <li role="menuitem"><roundcube:button command="event-history" type="link" label="calendar.eventhistory" classAct="active" /></li>
+ <roundcube:endif />
+ </ul>
+</div>
+
+<div id="eventdiff" class="uidialog eventdialog" aria-hidden="true">
+ <h1 class="event-title">Event Title</h1>
+ <h1 class="event-title-new event-text-new"></h1>
+ <div class="event-section event-date"></div>
+ <div class="event-section event-location">
+ <h5 class="label"><roundcube:label name="calendar.location" /></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-description">
+ <h5 class="label"><roundcube:label name="calendar.description" /></h5>
+ <div class="event-text-diff" style="white-space:pre-wrap"></div>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-url">
+ <h5 class="label"><roundcube:label name="calendar.url" /></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-recurrence">
+ <h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-section event-alarms">
+ <h5 class="label"><roundcube:label name="calendar.alarms" /><span class="index"></span></h5>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+ <div class="event-line event-start">
+ <label><roundcube:label name="calendar.start" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-end">
+ <label><roundcube:label name="calendar.end" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-attendees">
+ <label><roundcube:label name="calendar.tabattendees" /><span class="index"></span></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-calendar">
+ <label><roundcube:label name="calendar.calendar" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-categories">
+ <label><roundcube:label name="calendar.category" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-status">
+ <label><roundcube:label name="calendar.status" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-free_busy">
+ <label><roundcube:label name="calendar.freebusy" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-priority">
+ <label><roundcube:label name="calendar.priority" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-line event-sensitivity">
+ <label><roundcube:label name="calendar.sensitivity" /></label>
+ <span class="event-text-old"></span> &#8674;
+ <span class="event-text-new"></span>
+ </div>
+ <div class="event-section event-attachments">
+ <label><roundcube:label name="attachments" /><span class="index"></span></label>
+ <div class="event-text-old"></div>
+ <div class="event-text-new"></div>
+ </div>
+</div>
+
+<roundcube:include file="/templates/eventedit.html" />
+
+<div id="eventresourcesdialog" class="uidialog" aria-hidden="true">
+ <div id="resource-dialog-left">
+ <div id="resource-selection" class="uibox listbox" role="navigation" aria-labelledby="aria-label-resourceselection">
+ <h2 class="voice" id="aria-label-resourceselection"><roundcube:label name="calendar.arialabelresourceselection" /></h2>
+ <div id="resourcequicksearch">
+ <div class="searchbox" role="search" aria-labelledby="aria-label-resourcesearchform" aria-controls="resources-list">
+ <h3 id="aria-label-resourcesearchform" class="voice"><roundcube:label name="calendar.arialabelresourcesearchform" /></h3>
+ <label for="resourcesearchbox" class="voice"><roundcube:label name="calendar.searchterms" /></label>
+ <roundcube:object name="plugin.resources_searchform" id="resourcesearchbox" />
+ <a id="resourcesearchmenulink" class="iconbutton searchoptions"> </a>
+ <roundcube:button command="reset-resource-search" id="resourcesearchreset" class="iconbutton reset" title="resetsearch" label="resetsearch" />
+ </div>
+ </div>
+ <div class="scroller">
+ <roundcube:object name="plugin.resources_list" id="resources-list" class="listing treelist" />
+ </div>
+ </div>
+ </div>
+
+ <div id="resource-dialog-right">
+ <div id="resource-info" class="uibox contentbox" role="region" aria-labelledby="aria-label-resourcedetails">
+ <h2 class="boxtitle" id="aria-label-resourcedetails"><roundcube:label name="calendar.resourcedetails" /></h2>
+ <div class="scroller">
+ <roundcube:object name="plugin.resource_info" id="resource-details" class="propform" aria-live="polite" aria-relevant="text" aria-atomic="true" />
+ </div>
+ </div>
+
+ <div id="resource-availability" class="uibox contentbox" role="region" aria-labelledby="aria-label-resourceavailability">
+ <h2 class="boxtitle" id="aria-label-resourceavailability"><roundcube:label name="calendar.resourceavailability" /></h2>
+ <roundcube:object name="plugin.resource_calendar" id="resource-freebusy-calendar" />
+ <div class="boxpagenav">
+ <roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link" class="icon prevpage" title="calendar.prevslot" label="calendar.prevweek" />
+ <roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link" class="icon nextpage" title="calendar.nextslot" label="calendar.nextweek" />
+ </div>
+ </div>
+ </div>
+</div>
+
+<div id="eventfreebusy" class="uidialog" aria-hidden="true">
+ <roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellpadding="0" />
+
+ <div class="schedule-options">
+ &nbsp;
+ <div class="schedule-buttons">
+ <button id="shedule-freebusy-prev" title="<roundcube:label name='previouspage' />">&#9668;</button><button id="shedule-freebusy-next" title="<roundcube:label name='nextpage' />">&#9658;</button>
+ </div>
+ </div>
+
+ <div style="float:left; width:28em">
+ <div class="form-section">
+ <label for="schedule-startdate"><roundcube:label name="calendar.start" /></label>
+ <input type="text" name="startdate" size="11" id="schedule-startdate" disabled="true" /> &nbsp;
+ <input type="text" name="starttime" size="6" id="schedule-starttime" disabled="true" />
+ </div>
+ <div class="form-section">
+ <label for="schedule-enddate"><roundcube:label name="calendar.end" /></label>
+ <input type="text" name="enddate" size="11" id="schedule-enddate" disabled="true" /> &nbsp;
+ <input type="text" name="endtime" size="6" id="schedule-endtime" disabled="true" />
+ </div>
+ </div>
+ <div style="float:left">
+ <div class="schedule-find-buttons">
+ <button id="shedule-find-prev">&#9668; <roundcube:label name="calendar.prevslot" /></button>
+ <button id="shedule-find-next"><roundcube:label name="calendar.nextslot" /> &#9658;</button>
+ </div>
+ <div class="schedule-options">
+ <label><input type="checkbox" id="schedule-freebusy-workinghours" value="1" /><roundcube:label name="calendar.onlyworkinghours" /></label>
+ </div>
+ </div>
+ <br style="clear:both;" />
+
+ <roundcube:include file="/templates/freebusylegend.html" />
+ <div class="attendees-list">
+ <span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
+ <span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
+ <span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
+ <span class="attendee non-participant"><roundcube:label name="calendar.rolenonparticipant" /></span>
+ <span class="attendee chair"><roundcube:label name="calendar.rolechair" /></span>
+ </div>
+</div>
+
+<div id="eventhistory" class="uidialog" aria-hidden="true">
+ <roundcube:object name="plugin.object_changelog_table" id="event-changelog-table" class="records-table changelog-table" domain="calendar" />
+ <div class="compare-button"><input type="button" class="button" value="↳ <roundcube:label name='calendar.compare' />" /></div>
+</div>
+
+<div id="calendarform" class="uidialog" aria-hidden="true">
+ <roundcube:label name="loading" />
+</div>
+
+<div id="eventsimport" class="uidialog">
+ <roundcube:object name="plugin.events_import_form" id="events-import-form" uploadFieldSize="30" />
+</div>
+
+<div id="eventsexport" class="uidialog">
+ <roundcube:object name="plugin.events_export_form" id="events-export-form" />
+</div>
+
+<div id="calendarurlbox" class="uidialog">
+ <p><roundcube:label name="calendar.showurldescription" /></p>
+ <textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
+ <div id="calendarcaldavurl" style="display:none">
+ <p><roundcube:label name="calendar.caldavurldescription" html="yes" /></p>
+ <textarea id="caldavurl" rows="2" readonly="readonly"></textarea>
+ </div>
+</div>
+
+<roundcube:object name="plugin.calendar_css" />
+
+<script type="text/javascript">
+
+// UI startup
+var UI = new rcube_mail_ui();
+
+$(document).ready(function(e){
+ UI.init();
+
+ new calendarview_splitter({ id:'calsidebarsplitter', p1:'#calendarsidebar', p2:'#calendar',
+ orientation:'v', relative:true, start:280, min:260, size:12, offset:0 });
+
+ new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
+ orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
+
+ // animation to unfold list search box
+ $('#calendars .boxtitle a.search').click(function(e){
+ var title = $('#calendars .boxtitle'),
+ box = $('#calendars .listsearchbox'),
+ dir = box.is(':visible') ? -1 : 1;
+
+ if (!rcube_event.is_keyboard(e))
+ $(this).blur();
+
+ box.slideToggle({
+ duration: 160,
+ progress: function(animation, progress) {
+ if (dir < 0) progress = 1 - progress;
+ $('#calendars .scroller').css('top', (title.outerHeight() + 34 * progress) + 'px');
+ },
+ complete: function() {
+ box.toggleClass('expanded');
+ if (box.is(':visible')) {
+ box.find('input[type=text]').focus();
+ }
+ else {
+ $('#calendarlistsearch-reset').click();
+ }
+ // TODO: save state in localStorage
+ }
+ });
+
+ return false;
+ });
+
+});
+
+
+/**
+ * Extended rcube_splitter class that entirely collapses the calendar sidebar
+ */
+function calendarview_splitter(p)
+{
+ this.collapsed = false;
+ this.dragging = false;
+ this.threshold = 80;
+ this.lastpos = -1;
+ this._lastpos = -1;
+ this._min = p.min;
+
+ var me = this;
+ p.callback = function(e){
+ if (me.lastpos != me._lastpos) {
+ me.dragging = true;
+ setTimeout(function(){ me.dragging = false; }, 50);
+ me._lastpos = me.lastpos;
+ }
+ };
+
+ // extend base class
+ p.min = 20;
+ rcube_splitter.call(this, p);
+
+ // @override
+ this.resize = function()
+ {
+ if (this.pos < this.threshold) {
+ if (!this.collapsed)
+ this.collapse();
+ }
+ else if (this.pos < this._min && this.pos > this._min / 2) {
+ if (this.collapsed)
+ this.expand();
+ }
+ else if (this.pos >= this._min) {
+ this.p1.css('width', Math.floor(this.pos - this.p1pos.left - this.halfsize) + 'px');
+ this.p2.css('left', Math.ceil(this.pos + this.halfsize) + 'px');
+ this.handle.css('left', Math.round(this.pos - this.halfsize + this.offset + 3)+'px');
+ if (bw.ie) {
+ var new_width = parseInt(this.parent.outerWidth(), 10) - parseInt(this.p2.css('left'), 10) ;
+ this.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
+ }
+
+ this.p2.resize();
+ this.p1.resize();
+ this.lastpos = this.pos;
+
+ if (this._lastpos == -1)
+ this._lastpos = this.pos;
+
+ // also resize iframe covers
+ if (this.drag_active) {
+ $('iframe').each(function(i, elem) {
+ var pos = $(this).offset();
+ $('#iframe-splitter-fix-'+i).css({ top: pos.top+'px', left: pos.left+'px', width:elem.offsetWidth+'px', height: elem.offsetHeight+'px' });
+ });
+ }
+
+ if (typeof this.render == 'function')
+ this.render(this);
+ }
+ }
+
+ this.collapse = function(animated)
+ {
+ var me = this, time = 250;
+ if (animated) {
+ this.p1.animate({ left:'0px' }, time, function(){ $(this).hide(); });
+ this.p2.animate({ left:this.p.size + 'px' }, time, function(){ $(this).resize(); });
+ this.handle.animate({ left:'3px'}, time, function(){ $(this).addClass('sidebarclosed') });
+ }
+ else {
+ this.p1.css('left', 0).hide();
+ this.p2.css('left', this.p.size + 'px');
+ this.handle.css('left', '3px').addClass('sidebarclosed');
+ this.p2.resize();
+ }
+
+ // stop dragging
+ if (this.drag_active) {
+ this.drag_active = false;
+ $(document).unbind('.'+this.id);
+ $('div.iframe-splitter-fix').remove();
+ }
+
+ this.pos = 10;
+ this.collapsed = true;
+ this.set_cookie();
+ }
+
+ this.expand = function()
+ {
+ var me = this, time = 250;
+ this.handle.removeClass('sidebarclosed');
+ this.pos = this.lastpos > 0 ? this.lastpos : this._min;
+ this.p1pos.left = 10;
+ this.p1.show().animate({ left:'10px', width:(this.pos - this.p1pos.left - this.halfsize) + 'px' }, time);
+ this.p2.animate({ left:(this.pos + this.halfsize) + 'px' }, time, function(){ me.resize(); });
+ this.handle.animate({ left:(this.pos - this.halfsize + this.offset + 3) + 'px' }, time);
+
+ this.collapsed = false;
+ this.set_cookie();
+ }
+
+ this.init();
+
+ var me = this;
+ this.handle.bind('click', function(e){
+ if (!me.collapsed && !me.dragging)
+ me.collapse(true);
+ else if (!me.dragging)
+ me.expand();
+ });
+}
+
+</script>
+
+</body>
+</html>
diff --git a/calendar/skins/larry/templates/eventedit.html b/calendar/skins/larry/templates/eventedit.html
new file mode 100644
index 0000000..4d0585b
--- /dev/null
+++ b/calendar/skins/larry/templates/eventedit.html
@@ -0,0 +1,133 @@
+<div id="eventedit" class="uidialog uidialog-tabbed" aria-hidden="true">
+ <form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
+ <ul>
+ <li><a href="#event-panel-summary"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-panel-recurrence"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-panel-attendees"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-resources"><a href="#event-panel-resources"><roundcube:label name="calendar.tabresources" /></a></li><li id="edit-tab-attachments"><a href="#event-panel-attachments"><roundcube:label name="calendar.tabattachments" /></a></li>
+ </ul>
+ <!-- basic info -->
+ <div id="event-panel-summary">
+ <div class="event-section">
+ <label for="edit-title"><roundcube:label name="calendar.title" /></label>
+ <br />
+ <input type="text" class="text" name="title" id="edit-title" size="40" required="true" />
+ </div>
+ <div class="event-section">
+ <label for="edit-location"><roundcube:label name="calendar.location" /></label>
+ <br />
+ <input type="text" class="text" name="location" id="edit-location" size="40" />
+ </div>
+ <div class="event-section">
+ <label for="edit-description"><roundcube:label name="calendar.description" /></label>
+ <br />
+ <textarea name="description" id="edit-description" class="text" rows="5" cols="40"></textarea>
+ </div>
+ <div class="event-section">
+ <label for="edit-url"><roundcube:label name="calendar.url" /></label>
+ <br />
+ <input type="text" class="text" name="vurl" id="edit-url" size="40" />
+ </div>
+ <div class="event-section">
+ <label style="float:right;padding-right:0.5em"><input type="checkbox" name="allday" id="edit-allday" value="1" /><roundcube:label name="calendar.all-day" /></label>
+ <label for="edit-startdate"><roundcube:label name="calendar.start" /></label>
+ <input type="text" name="startdate" size="11" id="edit-startdate" required="true" /> &nbsp;
+ <input type="text" name="starttime" size="6" id="edit-starttime" aria-label="<roundcube:label name='calendar.starttime' />" />
+ </div>
+ <div class="event-section">
+ <label for="edit-enddate"><roundcube:label name="calendar.end" /></label>
+ <input type="text" name="enddate" size="11" id="edit-enddate" required="true" /> &nbsp;
+ <input type="text" name="endtime" size="6" id="edit-endtime" aria-label="<roundcube:label name='calendar.endtime' />" />
+ </div>
+ <div class="event-section" id="edit-alarms">
+ <div class="edit-alarm-item first">
+ <label for="edit-alarm-item"><roundcube:label name="calendar.alarms" /></label>
+ <roundcube:object name="plugin.alarm_select" id="edit-alarm-item" />
+ <span class="edit-alarm-buttons">
+ <a href="#add" class="iconbutton add add-alarm"><roundcube:label name="libcalendaring.addalarm" /></a>
+ <a href="#delete" class="iconbutton remove delete-alarm"><roundcube:label name="libcalendaring.removealarm" /></a>
+ </span>
+ </div>
+ </div>
+ <div class="event-section" id="calendar-select">
+ <label for="edit-calendar"><roundcube:label name="calendar.calendar" /></label>
+ <roundcube:object name="plugin.calendar_select" id="edit-calendar" />
+ </div>
+ <div class="event-section">
+ <label for="edit-categories"><roundcube:label name="calendar.category" /></label>
+ <roundcube:object name="plugin.category_select" id="edit-categories" />
+ </div>
+ <div class="event-section">
+ <label for="edit-event-status"><roundcube:label name="calendar.status" /></label>
+ <roundcube:object name="plugin.status_select" id="edit-event-status" />
+ </div>
+ <div class="event-section">
+ <label for="edit-free-busy"><roundcube:label name="calendar.freebusy" /></label>
+ <roundcube:object name="plugin.freebusy_select" id="edit-free-busy" />
+ </div>
+ <div class="event-section">
+ <label for="edit-priority"><roundcube:label name="calendar.priority" /></label>
+ <roundcube:object name="plugin.priority_select" id="edit-priority" />
+ </div>
+ <div class="event-section">
+ <label for="edit-sensitivity"><roundcube:label name="calendar.sensitivity" /></label>
+ <roundcube:object name="plugin.sensitivity_select" id="edit-sensitivity" />
+ </div>
+ <div class="event-section" id="edit-event-links">
+ <label><roundcube:label name="calendar.links" /></label>
+ <div class="event-text"></div>
+ <br style="clear:left">
+ </div>
+ </div>
+ <!-- recurrence settings -->
+ <div id="event-panel-recurrence">
+ <div class="event-section border-after">
+ <roundcube:object name="plugin.recurrence_form" part="frequency" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-daily">
+ <roundcube:object name="plugin.recurrence_form" part="daily" class="event-section" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-weekly">
+ <roundcube:object name="plugin.recurrence_form" part="weekly" class="event-section" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-monthly">
+ <roundcube:object name="plugin.recurrence_form" part="monthly" class="event-section" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-yearly">
+ <roundcube:object name="plugin.recurrence_form" part="yearly" class="event-section" />
+ </div>
+ <div class="recurrence-form" id="recurrence-form-until">
+ <roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
+ </div>
+ <div class="recurrence-form" id="recurrence-form-rdate">
+ <roundcube:object name="plugin.recurrence_form" part="rdate" class="event-section" />
+ </div>
+ </div>
+ <!-- attendees list -->
+ <div id="event-panel-attendees">
+ <h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="calendar.arialabeleventattendees" /></h3>
+ <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
+ <roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
+ <roundcube:include file="/templates/freebusylegend.html" />
+ </div>
+ <!-- resources list -->
+ <div id="event-panel-resources">
+ <h3 id="aria-label-resourcestable" class="voice"><roundcube:label name="calendar.arialabeleventresources" /></h3>
+ <roundcube:object name="plugin.attendees_list" id="edit-resources-table" class="records-table edit-attendees-table" coltitle="resource" aria-labelledby="aria-label-resourcestable" />
+ <roundcube:object name="plugin.resources_form" id="edit-resources-form" />
+ <roundcube:include file="/templates/freebusylegend.html" />
+ </div>
+ <!-- attachments list (with upload form) -->
+ <div id="event-panel-attachments">
+ <div id="edit-attachments">
+ <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" />
+ </div>
+ <div id="edit-attachments-form" role="region" aria-labelledby="aria-label-attachmentuploadform">
+ <h3 id="aria-label-attachmentuploadform" class="voice"><roundcube:label name="arialabelattachmentuploadform" /></h2>
+ <roundcube:object name="plugin.attachments_form" id="calendar-attachment-form" attachmentFieldSize="30" />
+ </div>
+ <roundcube:object name="plugin.filedroparea" id="event-panel-attachments" />
+ </div>
+ </form>
+
+ <roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="event-dialog-message" style="display:none" />
+ <roundcube:object name="plugin.edit_recurring_warning" class="event-dialog-message edit-recurring-warning" style="display:none" />
+ <div id="edit-localchanges-warning" class="event-dialog-message" style="display:none"><roundcube:label name="calendar.localchangeswarning" /></div>
+</div> \ No newline at end of file
diff --git a/calendar/skins/larry/templates/freebusylegend.html b/calendar/skins/larry/templates/freebusylegend.html
new file mode 100644
index 0000000..41de703
--- /dev/null
+++ b/calendar/skins/larry/templates/freebusylegend.html
@@ -0,0 +1,7 @@
+ <div id="edit-attendees-legend" class="availability">
+ <span class="legend"><img class="availabilityicon free" src="program/resources/blank.gif" /> <roundcube:label name="calendar.availfree" /></span>
+ <span class="legend"><img class="availabilityicon busy" src="program/resources/blank.gif" /> <roundcube:label name="calendar.availbusy" /></span>
+ <span class="legend"><img class="availabilityicon tentative" src="program/resources/blank.gif" /> <roundcube:label name="calendar.availtentative" /></span>
+ <!--<span class="legend"><img class="availabilityicon out-of-office" src="program/resources/blank.gif" /> <roundcube:label name="calendar.availoutofoffice" /></span>-->
+ <span class="legend"><img class="availabilityicon unknown" src="program/resources/blank.gif" /> <roundcube:label name="calendar.availunknown" /></span>
+ </div>
diff --git a/calendar/skins/larry/templates/itipattend.html b/calendar/skins/larry/templates/itipattend.html
new file mode 100644
index 0000000..545d018
--- /dev/null
+++ b/calendar/skins/larry/templates/itipattend.html
@@ -0,0 +1,37 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body class="extwin calendaritipattend">
+
+<div id="header">
+<div id="topline">
+ <div class="topright">
+ <a href="#close" class="closelink" onclick="self.close()"><roundcube:label name="close" /></a>
+ </div>
+</div>
+
+<div id="topnav">
+ <roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
+</div>
+
+<br style="clear:both" />
+</div>
+
+<div id="mainscreen">
+
+<div class="centerbox uibox">
+ <roundcube:object name="plugin.event_inviteform" />
+ <roundcube:object name="plugin.event_invitebox" class="calendar-invitebox" />
+ <roundcube:object name="plugin.event_rsvp_buttons" type="submit" iname="rsvp" id="event-rsvp" delegate="false" />
+ </form>
+</div>
+
+<roundcube:object name="message" id="message" />
+
+</div>
+
+</body>
+</html> \ No newline at end of file
diff --git a/calendar/skins/larry/templates/kolabacl.html b/calendar/skins/larry/templates/kolabacl.html
new file mode 100644
index 0000000..ed9b0c7
--- /dev/null
+++ b/calendar/skins/larry/templates/kolabacl.html
@@ -0,0 +1,26 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+<style type="text/css" media="screen">
+
+body.aclform {
+ background: #efefef;
+ margin: 0;
+}
+
+body.aclform .hint {
+ margin: 1em;
+}
+
+</style>
+</head>
+<body class="iframe aclform">
+
+<roundcube:object name="folderacl" />
+
+<roundcube:include file="/includes/footer.html" />
+
+</body>
+</html>
diff --git a/calendar/skins/larry/templates/kolabform.html b/calendar/skins/larry/templates/kolabform.html
new file mode 100644
index 0000000..77a1c30
--- /dev/null
+++ b/calendar/skins/larry/templates/kolabform.html
@@ -0,0 +1,9 @@
+<div id="calendar-kolabform" class="propform tabbed">
+ <roundcube:object name="calendarform" />
+</div>
+<style type="text/css">
+#calendarpropform { min-width:680px; margin-top:-12px; }
+</style>
+<script type="text/javascript">
+UI.init_tabs('#calendar-kolabform');
+</script> \ No newline at end of file
diff --git a/calendar/skins/larry/templates/print.html b/calendar/skins/larry/templates/print.html
new file mode 100644
index 0000000..e679f72
--- /dev/null
+++ b/calendar/skins/larry/templates/print.html
@@ -0,0 +1,29 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+</head>
+<body class="calendarprint">
+
+<div id="printconfig" class="noprint">
+ <div class="calwidth">
+ <a href="#close" onclick="window.close()" class="rightalign"><roundcube:label name="close" /></a>
+ <span class="prop"><input type="button" id="printme" value="<roundcube:label name='print' />" onclick="window.print()"></span>
+ <span class="prop"><label><input type="checkbox" id="propdescription" checked="checked" /> <roundcube:label name="calendar.printdescriptions" /></label></span>
+ </div>
+</div>
+
+<roundcube:object name="message" id="message" class="noprint" />
+
+<div id="calendar" class="calwidth"></div>
+
+<div class="calwidth">
+ <roundcube:object name="plugin.calendar_list" activeonly="true" id="calendarlist" />
+ <br style="clear:both">
+</div>
+
+<roundcube:object name="plugin.calendar_css" printmode="true" />
+
+<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="plugins/calendar/skins/classic/print.iehacks.css" /><![endif]-->
+</body>
+</html> \ No newline at end of file

© 2014-2024 Faster IT GmbH | imprint | privacy policy