From 79f401590688553c34ba28fe1b8d41efd1005c08 Mon Sep 17 00:00:00 2001 From: Stefan Liebl Date: Thu, 9 Jul 2020 15:41:32 +0200 Subject: [PATCH] roundcube --- .gitignore | 1 + README.md | 13 +- docker-compose.yaml | 26 + fpm.nginx.conf | 40 + plugins/carddav/.gitignore | 3 + plugins/carddav/ChangeLog | 52 + plugins/carddav/INSTALL | 37 + plugins/carddav/INSTALLFROMGIT.md | 5 + plugins/carddav/LICENSE | 339 +++ plugins/carddav/README.md | 47 + plugins/carddav/carddav.php | 684 +++++ plugins/carddav/carddav_backend.php | 2460 +++++++++++++++++ plugins/carddav/carddav_common.php | 397 +++ plugins/carddav/carddav_discovery.php | 280 ++ plugins/carddav/composer.json | 38 + plugins/carddav/composer.lock | 136 + plugins/carddav/config.inc.php.dist | 217 ++ .../dbmigrations/0000-dbinit/mysql.sql | 84 + .../dbmigrations/0000-dbinit/postgres.sql | 105 + .../dbmigrations/0000-dbinit/sqlite3.sql | 89 + .../dbmigrations/0000-sample/mysql.sql | 1 + .../dbmigrations/0000-sample/postgres.sql | 1 + .../dbmigrations/0000-sample/sqlite3.sql | 1 + .../dbmigrations/0001-categories/mysql.sql | 1 + .../dbmigrations/0001-categories/postgres.sql | 1 + .../dbmigrations/0001-categories/sqlite3.sql | 1 + .../0002-increasetextfieldlengths/mysql.sql | 2 + .../postgres.sql | 2 + .../0002-increasetextfieldlengths/sqlite3.sql | 32 + .../0003-fixtimestampdefaultvalue/README.md | 1 + .../0003-fixtimestampdefaultvalue/mysql.sql | 1 + .../postgres.sql | 1 + .../0003-fixtimestampdefaultvalue/sqlite3.sql | 1 + .../0004-fixtimestampdefaultvalue/mysql.sql | 1 + .../postgres.sql | 1 + .../0004-fixtimestampdefaultvalue/sqlite3.sql | 1 + .../0005-changemysqlut8toutf8mb4/mysql.sql | 56 + .../0005-changemysqlut8toutf8mb4/postgres.sql | 1 + .../0005-changemysqlut8toutf8mb4/sqlite3.sql | 1 + plugins/carddav/localization/cs_CZ.inc | 17 + plugins/carddav/localization/de_DE.inc | 21 + plugins/carddav/localization/en_US.inc | 21 + plugins/carddav/localization/es_ES.inc | 17 + plugins/carddav/localization/fr_FR.inc | 20 + plugins/carddav/localization/hu_HU.inc | 17 + plugins/carddav/localization/id_ID.inc | 17 + plugins/carddav/localization/it_IT.inc | 17 + plugins/carddav/localization/pl_PL.inc | 18 + plugins/carddav/localization/ru_RU.inc | 17 + plugins/carddav/localization/sv_SE.inc | 17 + plugins/carddav/localization/uk_UK.inc | 17 + plugins/carddav/package.xml | 75 + plugins/carddav/skins/classic/carddav.css | 6 + plugins/carddav/skins/elastic/carddav.css | 3 + plugins/carddav/skins/larry/carddav.css | 6 + plugins/managesieve/config.inc.php | 110 + roundcube.config.php | 101 + 57 files changed, 5675 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 docker-compose.yaml create mode 100644 fpm.nginx.conf create mode 100644 plugins/carddav/.gitignore create mode 100644 plugins/carddav/ChangeLog create mode 100644 plugins/carddav/INSTALL create mode 100644 plugins/carddav/INSTALLFROMGIT.md create mode 100644 plugins/carddav/LICENSE create mode 100644 plugins/carddav/README.md create mode 100644 plugins/carddav/carddav.php create mode 100644 plugins/carddav/carddav_backend.php create mode 100644 plugins/carddav/carddav_common.php create mode 100644 plugins/carddav/carddav_discovery.php create mode 100644 plugins/carddav/composer.json create mode 100644 plugins/carddav/composer.lock create mode 100644 plugins/carddav/config.inc.php.dist create mode 100644 plugins/carddav/dbmigrations/0000-dbinit/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0000-dbinit/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0000-dbinit/sqlite3.sql create mode 100644 plugins/carddav/dbmigrations/0000-sample/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0000-sample/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0000-sample/sqlite3.sql create mode 100644 plugins/carddav/dbmigrations/0001-categories/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0001-categories/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0001-categories/sqlite3.sql create mode 100644 plugins/carddav/dbmigrations/0002-increasetextfieldlengths/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0002-increasetextfieldlengths/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0002-increasetextfieldlengths/sqlite3.sql create mode 100644 plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/README.md create mode 100644 plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/sqlite3.sql create mode 100644 plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/sqlite3.sql create mode 100644 plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/mysql.sql create mode 100644 plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/postgres.sql create mode 100644 plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/sqlite3.sql create mode 100644 plugins/carddav/localization/cs_CZ.inc create mode 100644 plugins/carddav/localization/de_DE.inc create mode 100644 plugins/carddav/localization/en_US.inc create mode 100644 plugins/carddav/localization/es_ES.inc create mode 100644 plugins/carddav/localization/fr_FR.inc create mode 100644 plugins/carddav/localization/hu_HU.inc create mode 100644 plugins/carddav/localization/id_ID.inc create mode 100644 plugins/carddav/localization/it_IT.inc create mode 100644 plugins/carddav/localization/pl_PL.inc create mode 100644 plugins/carddav/localization/ru_RU.inc create mode 100644 plugins/carddav/localization/sv_SE.inc create mode 100644 plugins/carddav/localization/uk_UK.inc create mode 100644 plugins/carddav/package.xml create mode 100644 plugins/carddav/skins/classic/carddav.css create mode 100644 plugins/carddav/skins/elastic/carddav.css create mode 100644 plugins/carddav/skins/larry/carddav.css create mode 100644 plugins/managesieve/config.inc.php create mode 100644 roundcube.config.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65eef93 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +db diff --git a/README.md b/README.md index 05326dc..13d5c27 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ -# docker-compose-workshop +Update, run, inspect and stop server: +``` +docker-compose pull +docker-compose up -d +docker-compose logs -t -f +docker-compose down +``` +Login to roundcube +``` +docker-compose exec roundcube sh -Docker-Compose workshop \ No newline at end of file +``` diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..a2f328b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,26 @@ +version: '2' +services: + roundcube: + image: instrumentisto/roundcube:1.4.7-fpm + expose: + - "9000" + volumes: + - app-volume:/app + - ./db:/var/db + - ./roundcube.config.php:/app/config/config.inc.php:ro + - ./plugins/identity_smtp:/app/plugins/identity_smtp + - ./plugins/carddav:/app/plugins/carddav + - ./plugins/managesieve/config.inc.php:/app/plugins/managesieve/config.inc.php + + nginx: + image: nginx:1.16.0-alpine + depends_on: + - roundcube + ports: + - "8081:80" + volumes: + - app-volume:/var/www + - ./fpm.nginx.conf:/etc/nginx/conf.d/default.conf:ro + +volumes: + app-volume: diff --git a/fpm.nginx.conf b/fpm.nginx.conf new file mode 100644 index 0000000..92ea10f --- /dev/null +++ b/fpm.nginx.conf @@ -0,0 +1,40 @@ + +server { + listen 80; + root /var/www/html; + index index.php; + charset utf-8; + + location = /favicon.ico { + root /var/www/html/skins/larry/images; + access_log off; + log_not_found off; + expires max; + } + + location = /robots.txt { + allow all; + access_log off; + log_not_found off; + } + + location / { + try_files $uri $uri/ index.php; + } + + location ~ /\.ht { + deny all; + access_log off; + log_not_found off; + } + + location ~* \.php$ { + try_files $uri =404; + include fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass roundcube:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_index index.php; + + } +} diff --git a/plugins/carddav/.gitignore b/plugins/carddav/.gitignore new file mode 100644 index 0000000..7381b8f --- /dev/null +++ b/plugins/carddav/.gitignore @@ -0,0 +1,3 @@ +config.inc.php +composer.phar +/vendor diff --git a/plugins/carddav/ChangeLog b/plugins/carddav/ChangeLog new file mode 100644 index 0000000..9e42b76 --- /dev/null +++ b/plugins/carddav/ChangeLog @@ -0,0 +1,52 @@ +Changes from 0.7.0 to 0.8.0: + * Implement sync_token support for less overhead when syncing + * Use XPath XML parsing instead of self-rolled functions + * Automatically detect all addressbooks on server + * Only use sync methods the server advertises + * many small changes + +Changes from 0.6.1 to 0.7.0: + * merge branch: cache + * support caching of entire addressbooks, thus speeding up repetetive queries + * add localizations: italian, french, swedish, hungarian + * improvements in handling multiple addressbooks + * improvements in handling different CardDAV servers + +Changes from 0.6.0 to 0.6.1: + * merge branch: multipleaddressbooks + * support multiple authorization header elements, patch by Michael Stilkerich + * adapt config file and its handling to new multiple addressbook feature + * fix FS#25 attaching photo + * fix FS#27 use surname lastname if no nickname provided + * fix FS#29 displayname/nickname handling + +Changes from 0.4.2 to 0.6.0: + * add group support + * faster vcard retrieval + * vcard parsing with library + * support for configuration file + * support full-featured addressbook of RC 0.6 + +Changes from 0.4.1 to 0.4.2: + * add some debug entries + * fix FS#7 - case-insensitive Authentication headers (Basic vs. basic, Digest vs. digest) + +Changes from 0.4.0 to 0.4.1: + * use PHP HTTP Client class for the whole HTTP layer handling. This speeds up processing from taking 15s to being "snappy". + +Changes from 0.3.1 to 0.4.0: + * check if PHP_VERSION >= 5.3.0 and display an error message in the preferences tab if too old. + This is because of https://bugs.php.net/bug.php?id=46035 + * fix FS#14 - add function get_name + * do not use + for delimiters, but _ as rcm 0.6 will substitute + for _ + +Changes from 0.3.0 to 0.3.1: + + * fix FS#10 - incomplete searchresults now complete + * fixes FS#11 - do not use ob_* functions but a custom error handler to catch fopen errors + * make password inputfield a password html-element to prevent shouldersurfing of password + * make size of url input field as long as the value, but at least 40 + * remove stray write_log in myErrorHandler + * make list_records return false instead of an empty result list if a search for an entry returned no results + * use tag for ID parsing instead of UID parameter in vCard + * try harder to authenticate against server - especially useful for PHP 5.2.x diff --git a/plugins/carddav/INSTALL b/plugins/carddav/INSTALL new file mode 100644 index 0000000..6715423 --- /dev/null +++ b/plugins/carddav/INSTALL @@ -0,0 +1,37 @@ +0. Log out of Roundcube! This is necessary for proper setup of the databases later. + +1a. Download the last release of RCMCardDAV here: + https://github.com/blind-coder/rcmcarddav/releases/tag/v3.0.3 + Extract the archive and rename the extracted directory to 'carddav'. + +1b. Copy/Clone the whole RCMCardDAV plugin directory to roundcubemail/plugins/ and rename +the directory to 'carddav'. + +2. Only if you are installing a git clone, change to the directory +roundcubemail/plugins/carddav/ and install the dependencies using composer: + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" + php composer-setup.php + php -r "unlink('composer-setup.php');" + php composer.phar install + +3. Change to the plugin directory roundcubemail/plugins/carddav and copy the +file 'config.inc.php.dist' to 'config.inc.php' to create the initial plugin +configuration file. + +4. If necessary, customize the default plugin settings in the file created in +step 3. + +5. Make sure that the file and directory ownership match the user which is used +to run your web server, e.g.: + chown -R wwwrun roundcubemail/plugins/carddav + chgrp -R nogroup roundcubemail/plugins/carddav + +6. Install the curl and mbstring php extensions if not already present: + apt-get install php7.0-{mbstring,curl} # on Debian + +7. Add 'carddav' to the $rcmail_config['plugins'] array in +roundcubemail/config/main.inc.php + +8. Login to Roundcube and set-up your first CardDAV address book by accessing +'Settings -> Settings -> CardDAV' diff --git a/plugins/carddav/INSTALLFROMGIT.md b/plugins/carddav/INSTALLFROMGIT.md new file mode 100644 index 0000000..81f2d6d --- /dev/null +++ b/plugins/carddav/INSTALLFROMGIT.md @@ -0,0 +1,5 @@ + - Clone the repository: + `cd roundcube/plugins && git clone https://github.com/blind-coder/rcmcarddav.git carddav` + - Install dependencies: + - Install composer as per the documentation: https://getcomposer.org/download/ + - Run `php composer.phar install` diff --git a/plugins/carddav/LICENSE b/plugins/carddav/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/plugins/carddav/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/plugins/carddav/README.md b/plugins/carddav/README.md new file mode 100644 index 0000000..b7db61a --- /dev/null +++ b/plugins/carddav/README.md @@ -0,0 +1,47 @@ +RCMCardDAV +========== + +CardDAV plugin for the RoundCube Webmailer + +Upgrading from 1.0 +================== + +There is no upgrade path from the 1.0 version. You need to manually remove RCMCardDAV 1.0, drop its tables from your database and start with a fresh installation. + +Upgrading from 2.0.x +================== + +There is no supported upgrade path from the 2.0.x version. You need to manually remove RCMCardDAV 2.0.x, drop its tables from your database and start with a fresh installation. + +Requirements +============ +RCMCardDAV requires at least PHP 5.6.18. Older versions might work if the version check is disabled using the `$prefs['_GLOBAL']['suppress_version_warning']` configuration entry, but this is unsupported. + +Installation +============ + +RCMCardDAV can be installed via composer, from a release tarball or from a git clone. This list is in increasing difficulty, with composer being the easiest method. + +Please note that due to version incompatibilities of depending libraries, this plugin might be incompatible to Kolabs calendar plugin. There is a compatible version available here: `http://git.faster-it.de/roundcube_calendar/`. + +Intallation steps: +- Log out of Roundcube! + This is important because RCMCardDAV runs its database initialisation / update procedure only when a user logs in! +- Get RCMCardDAV + - Via composer: + - Add `"roundcube/carddav": "dev-master"` to your composer.json file and install with `php composer.phar install`. + - Via release tarball: + - Download and extract the release tarball into `roundcube/plugins` directory and rename the extracted directory to `carddav`. The tarball contains all necessary dependencies and does not need composer. + - Via git: + - Please do not do not do this unless you have a very good reason for it! Check the file [INSTALLFROMGIT.md](INSTALLFROMGIT.md) for instructions. +- Configure RCMCardDAV + If you want to configure preset addressbooks for your users, copy the file `config.inc.php.dist` to `config.inc.php` and edit it as you need. +- Make sure that the files and directories are owned by the user and group that your webserver runs as. For Debian GNU/Linux that would be: + `chown -R www-data:www-data roundcubemail/plugins/carddav` +- Install the curl php extension if not already present: + `sudo apt-get install php5-curl` +- Enable RCMCardDAV in Roundcube: + Open the file `roundcube/config/config.inc.php` and add `carddav` to the array `$config['plugins']`. +- Login to Roundcube and setup your addressbook by navigation to the Settings page and click on CardDAV. + +In case of errors, check the files `roundcube/logs/*`. diff --git a/plugins/carddav/carddav.php b/plugins/carddav/carddav.php new file mode 100644 index 0000000..b05d4d6 --- /dev/null +++ b/plugins/carddav/carddav.php @@ -0,0 +1,684 @@ +, + Michael Stilkerich + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ +class carddav extends rcube_plugin +{ + private static $helper; + + // the dummy task is used by the calendar plugin, which requires + // the addressbook to be initialized + public $task = 'addressbook|login|mail|settings|dummy'; + + public function checkMigrations(){ + $dbh = rcmail::get_instance()->db; + + $db_backend = "unknown"; + + switch ($dbh->db_provider){ + case "mysql": + $db_backend = "mysql"; + break; + case "sqlite": + $db_backend = "sqlite3"; + break; + case "pgsql": + case "postgres": + $db_backend = "postgres"; + break; + } + + if ($db_backend == "unknown"){ + rcmail::write_log("carddav", "Unknown database backend: ".$dbh->db_provider); + return; + } + + # first initialize the carddav_migrations table if it doesn't exist. + /* + $query = file_get_contents(dirname(__FILE__)."/dbinit/".$db_backend.".sql"); + if (strlen($query) > 0){ + $query = str_replace("TABLE_PREFIX", $config->get('db_prefix', ""), $query); + $dbh->query($query); + rcmail::write_log("carddav", "Processed initialization of carddav_migrations table"); + } else { + rcmail::write_log("carddav", "Can't find migration: /dbinit/".$db_backend.".sql"); + } + */ + + $config = rcmail::get_instance()->config; + $migrations = array_diff(scandir(dirname(__FILE__)."/dbmigrations/"), array('..', '.')); + $mignew = array(); + foreach ($migrations as $k => $v){ + $mignew[] = $v; + } + $migrations = $mignew; + $qmarks = "?"; + for ($i=1;$iset_option('ignore_key_errors', true); + $sql_result = $dbh->query('SELECT * FROM '. + $dbh->table_name('carddav_migrations') . + ' WHERE filename IN ('.$qmarks.');', $migrations); + + if ($sql_result){ + while ($processed = $dbh->fetch_assoc($sql_result)) { + if(($key = array_search($processed['filename'], $migrations)) !== false) { + unset($migrations[$key]); + } + } + } + $dbh->set_option('ignore_key_errors', null); + + foreach ($migrations as $migration) { + rcmail::write_log('carddav', "In migration: ".$migration); + $queries_raw = file_get_contents(dirname(__FILE__)."/dbmigrations/".$migration."/".$db_backend.".sql"); + $match_count = preg_match_all('/(.+?;)/s', $queries_raw, $matches); + rcmail::write_log('carddav', 'Found '.$match_count.' matches'); + if($match_count > 0){ + foreach ($matches[0] as $query){ // array will have two elements, each holding all queries. Only iterate over one of them + if (strlen($query) > 0){ + $query = str_replace("TABLE_PREFIX", $config->get('db_prefix', ""), $query); + $dbh->query($query); + } + } + $dbh->query("INSERT INTO ".$dbh->table_name("carddav_migrations")." (filename) VALUES (?)", $migration); + }else{ + rcmail::write_log('carddav', "Did not match any instructions from migration ".$migration); + } + } + } + + public function init() + {{{ + $this->rc = rcmail::get_instance(); + $tasks = explode('|', $this->task); + + // Since other plugins may also use the Sabre library + // In order to avoid version conflicts between Sabre libraries + // which might be used by other plugins + // It is better to restrict the loading of Sabre library + // under necessary tasks + if(!in_array($this->rc->task, $tasks)) + return; + else { + require_once('carddav_backend.php'); + require_once('carddav_discovery.php'); + require_once('carddav_common.php'); + } + + self::$helper = new carddav_common('BACKEND: '); + + $this->add_hook('addressbooks_list', array($this, 'address_sources')); + $this->add_hook('addressbook_get', array($this, 'get_address_book')); + + $this->add_hook('preferences_list', array($this, 'cd_preferences')); + $this->add_hook('preferences_save', array($this, 'cd_save')); + $this->add_hook('preferences_sections_list',array($this, 'cd_preferences_section')); + + $this->add_hook('login_after',array($this, 'checkMigrations')); + $this->add_hook('login_after',array($this, 'init_presets')); + + if(!array_key_exists('user_id', $_SESSION)) + return; + + // use this address book for autocompletion queries + // (maybe this should be configurable by the user?) + $config = rcmail::get_instance()->config; + $sources = (array) $config->get('autocomplete_addressbooks', array('sql')); + + $dbh = rcmail::get_instance()->db; + $sql_result = $dbh->query('SELECT id FROM ' . + $dbh->table_name('carddav_addressbooks') . + ' WHERE user_id=? AND active=1', + $_SESSION['user_id']); + + while ($abookrow = $dbh->fetch_assoc($sql_result)) { + $abookname = "carddav_" . $abookrow['id']; + if (!in_array($abookname, $sources)) { + $sources[] = $abookname; + } + } + $config->set('autocomplete_addressbooks', $sources); + $skin_path = $this->local_skin_path(); + $this->include_stylesheet($skin_path . '/carddav.css'); + }}} + + public function init_presets() + {{{ + $dbh = rcmail::get_instance()->db; + $prefs = carddav_common::get_adminsettings(); + + // migrate old settings + carddav_backend::migrateconfig(); + + // read existing presets from DB + $sql_result = $dbh->query('SELECT * FROM ' . + $dbh->table_name('carddav_addressbooks') . + ' WHERE user_id=? AND presetname is not null', + $_SESSION['user_id']); + + $existing_presets = array( ); + while ($abookrow = $dbh->fetch_assoc($sql_result)) { + $pn = $abookrow['presetname']; + if(!array_key_exists($pn,$existing_presets)) { + $existing_presets[$pn] = array(); + } + $existing_presets[$pn][] = $abookrow; + } + + // add not existing preset addressbooks + foreach($prefs as $presetname => $preset) { + if($presetname === '_GLOBAL') continue; + + // addressbooks exist for this preset => update settings + if(array_key_exists($presetname, $existing_presets)) { + if(is_array($preset['fixed'])) { + // update all existing addressbooks for this preset + foreach($existing_presets[$presetname] as $abookrow) { + // decrypt password so that the comparison works + $abookrow['password'] = self::$helper->decrypt_password($abookrow['password']); + + // update: only admin fix keys, only if it's fixed + // otherwise there may be user changes that should not be destroyed + $pa = array(); + + foreach($preset['fixed'] as $k) { + if(array_key_exists($k, $abookrow) && array_key_exists($k,$preset)) { + + // only update the name if it is used + if($k === 'name') { + if(!$preset['carddav_name_only']) { + $fullname = $abookrow['name']; + $cnpos = strpos($fullname, ' ('); + if($cnpos === FALSE && strcmp($preset['name'],$fullname)!==0) { + $pa['name'] = $preset['name']; + } else if($cnpos !== FALSE && strcmp($preset['name'],substr($fullname,0,$cnpos))!==0) { + $pa['name'] = $preset['name'] . substr($fullname, $cnpos); + } + } + + } else if ($abookrow[$k] != $preset[$k]) { + $pa[$k] = $preset[$k]; + } + } + } + + // only update if something changed + if(count($pa)===0) continue; + + self::update_abook($abookrow['id'],$pa); + } + } + + unset($existing_presets[$presetname]); + + } else { // create new + $preset['presetname'] = $presetname; + $preset['password'] = self::$helper->encrypt_password($preset['password']); + $abname = $preset['name']; + + $discovery = new carddav_discovery(); + $srvs = $discovery->find_addressbooks($preset['url'], $preset['username'], $preset['password']); + + if(is_array($srvs)) { + foreach($srvs as $srv){ + if($srv['name']) { + if($preset['carddav_name_only']) + $preset['name'] = $srv['name']; + else + $preset['name'] = "$abname (" . $srv['name'] . ')'; + } else { + $preset['name'] = $abname; + } + $preset['url'] = $srv['href']; + self::insert_abook($preset); + }} + } + } + + // delete existing preset addressbooks that where removed by admin + foreach($existing_presets as $ep) { + foreach($ep as $abookrow) { + self::delete_abook($abookrow['id']); + } + } + }}} + + public function address_sources($p) + {{{ + $dbh = rcmail::get_instance()->db; + $prefs = carddav_common::get_adminsettings(); + + $sql_result = $dbh->query('SELECT id,name,presetname FROM ' . + $dbh->table_name('carddav_addressbooks') . + ' WHERE user_id=? AND active=1', + $_SESSION['user_id']); + + while ($abookrow = $dbh->fetch_assoc($sql_result)) { + $ro = false; + if($abookrow['presetname'] && $prefs[$abookrow['presetname']]['readonly']) + $ro = true; + + $p['sources']["carddav_".$abookrow['id']] = array( + 'id' => "carddav_".$abookrow['id'], + 'name' => $abookrow['name'], + 'groups' => true, + 'autocomplete' => true, + 'readonly' => $ro, + ); + } + return $p; + }}} + + public function get_address_book($p) + {{{ + if (preg_match(";^carddav_(\d+)$;", $p['id'], $match)){ + $p['instance'] = new carddav_backend($match[1]); + } + + return $p; + }}} + + private static function process_cd_time($refresht) + {{{ + if(preg_match('/^(\d+)(:([0-5]?\d))?(:([0-5]?\d))?$/', $refresht, $match)) { + $refresht = sprintf("%02d:%02d:%02d", $match[1], + count($match)>3 ? $match[3] : 0, + count($match)>5 ? $match[5] : 0); + } else { + $refresht = '01:00:00'; + } + return $refresht; + }}} + + private static function no_override($pref, $abook, $prefs) + {{{ + $pn = $abook['presetname']; + if(!$pn) return false; + + // never enable user change for preset URLs + if($pref === 'url') return true; + + if(!is_array($prefs[$pn])) return false; + if(!is_array($prefs[$pn]['fixed'])) return false; + + return in_array($pref,$prefs[$pn]['fixed']); + }}} + + /** + * Builds a setting block for one address book for the preference page. + */ + private function cd_preferences_buildblock($blockheader,$abook,$prefs) + {{{ + $abookid = $abook['id']; + $rcmail = rcmail::get_instance(); + + if (self::no_override('active', $abook, $prefs)) { + $content_active = $prefs[$abook['presetname']] ? $this->gettext('cd_enabled') : $this->gettext('cd_disabled'); + } else { + // check box for activating + $checkbox = new html_checkbox(array('name' => $abookid.'_cd_active', 'value' => 1)); + $content_active = $checkbox->show($abook['active']?1:0); + } + + if (self::no_override('use_categories', $abook, $prefs) || $abook['id'] !== "new") { + $content_use_categories = $abook['use_categories'] ? $this->gettext('cd_enabled') : $this->gettext('cd_disabled'); + } else { + // check box for use categories + $checkbox = new html_checkbox(array('name' => $abookid.'_cd_use_categories', 'value' => 1)); + $content_use_categories = $checkbox->show($abook['use_categories']?1:0); + } + + if (self::no_override('username', $abook, $prefs)) { + // %V parses username for macosx, replaces periods and @ by _, work around bugs in contacts.app + $content_username = $abook['username'] === '%V' ? str_replace('@','_', str_replace('.','_',$_SESSION['username'])) : $abook['username'] === '%u' ? $_SESSION['username'] : $abook['username'] === '%l' ? $rcmail->user->get_username('local') : $abook['username']; + + } else { + // input box for username + $input = new html_inputfield(array('name' => $abookid.'_cd_username', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['username'])); + $content_username = $input->show(); + } + + if (self::no_override('password', $abook, $prefs)) { + $content_password = "***"; + } else { + // input box for password + $input = new html_inputfield(array('name' => $abookid.'_cd_password', 'type' => 'password', 'autocomplete' => 'off', 'value' => '')); + $content_password = $input->show(); + } + + if (self::no_override('url', $abook, $prefs)) { + $content_url = str_replace("%u", $abook['username'], $abook['url']); + } else { + // input box for URL + $size = max(strlen($abook['url']),40); + $input = new html_inputfield(array('name' => $abookid.'_cd_url', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['url'], 'size' => $size)); + $content_url = $input->show(); + } + + // input box for refresh time + if (self::no_override('refresh_time', $abook, $prefs)) { + $content_refresh_time = $abook['refresh_time']; + } else { + $input = new html_inputfield(array('name' => $abookid.'_cd_refresh_time', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['refresh_time'], 'size' => 10)); + $content_refresh_time = $input->show(); + } + + if (self::no_override('name', $abook, $prefs)) { + $content_name = $abook['name']; + } else { + $input = new html_inputfield(array('name' => $abookid.'_cd_name', 'type' => 'text', 'autocomplete' => 'off', 'value' => $abook['name'], 'size' => 40)); + $content_name = $input->show(); + } + + $retval = array( + 'options' => array( + array('title'=> self::$helper->Q($this->gettext('cd_name')), 'content' => $content_name), + array('title'=> self::$helper->Q($this->gettext('cd_active')), 'content' => $content_active), + array('title'=> self::$helper->Q($this->gettext('cd_use_categories')), 'content' => $content_use_categories), + array('title'=> self::$helper->Q($this->gettext('cd_username')), 'content' => $content_username), + array('title'=> self::$helper->Q($this->gettext('cd_password')), 'content' => $content_password), + array('title'=> self::$helper->Q($this->gettext('cd_url')), 'content' => $content_url), + array('title'=> self::$helper->Q($this->gettext('cd_refresh_time')), 'content' => $content_refresh_time), + ), + 'name' => $blockheader + ); + + if (!$abook['presetname'] && preg_match('/^\d+$/',$abookid)) { + $checkbox = new html_checkbox(array('name' => $abookid.'_cd_delete', 'value' => 1)); + $content_delete = $checkbox->show(0); + $retval['options'][] = array('title'=> self::$helper->Q($this->gettext('cd_delete')), 'content' => $content_delete); + } + + return $retval; + }}} + + // user preferences + function cd_preferences($args) + {{{ + if($args['section'] != 'cd_preferences') + return; + + $this->include_stylesheet($this->local_skin_path().'/carddav.css'); + $this->add_texts('localization/', false); + $prefs = carddav_common::get_adminsettings(); + + if (!$prefs['_GLOBAL']['suppress_version_warning']){ + if (version_compare(PHP_VERSION, '5.6.18', '<')) { + $args['blocks']['cd_preferences'] = array( + 'options' => array( + array('title'=> self::$helper->Q($this->gettext('cd_php_too_old')), 'content' => PHP_VERSION) + ), + 'name' => self::$helper->Q($this->gettext('cd_title')) + ); + return $args; + } + } + + $abooks = carddav_backend::get_dbrecord($_SESSION['user_id'],'*','addressbooks',false,'user_id'); + foreach($abooks as $abook) { + $presetname = $abook['presetname']; + if (empty($presetname) || + (!isset($prefs[$presetname]['hide']) || (isset($prefs[$presetname]['hide']) && $prefs[$presetname]['hide'] === FALSE))) { + $abookid = $abook['id']; + $blockhdr = $abook['name']; + if($abook['presetname']) + $blockhdr .= str_replace("_PRESETNAME_", $abook['presetname'], self::$helper->Q($this->gettext('cd_frompreset'))); + $args['blocks']['cd_preferences'.$abookid] = $this->cd_preferences_buildblock($blockhdr,$abook,$prefs); + } + } + + if(!array_key_exists('_GLOBAL', $prefs) || !$prefs['_GLOBAL']['fixed']) { + $args['blocks']['cd_preferences_section_new'] = $this->cd_preferences_buildblock( + self::$helper->Q($this->gettext('cd_newabboxtitle')), + array( + 'id' => 'new', + 'active' => 1, + 'use_categories' => 1, + 'username' => '', + 'url' => '', + 'name' => '', + 'refresh_time' => 1, + 'presetname' => '', + ), $prefs); + } + + return($args); + }}} + + // add a section to the preferences tab + function cd_preferences_section($args) + {{{ + $prefs = carddav_common::get_adminsettings(); + if (!isset($prefs['_GLOBAL']['hide_preferences']) || (isset($prefs['_GLOBAL']['hide_preferences']) && $prefs['_GLOBAL']['hide_preferences'] === FALSE)) { + $this->add_texts('localization/', false); + $args['list']['cd_preferences'] = array( + 'id' => 'cd_preferences', + 'section' => self::$helper->Q($this->gettext('cd_title')) + ); + } + return($args); + }}} + + // save preferences + function cd_save($args) + {{{ + $this->add_texts('localization/', false); + if($args['section'] != 'cd_preferences') + return; + $prefs = carddav_common::get_adminsettings(); + if (isset($prefs['_GLOBAL']['hide_preferences']) && $prefs['_GLOBAL']['hide_preferences'] === TRUE) { + return; + } + + // update existing in DB + $abooks = carddav_backend::get_dbrecord($_SESSION['user_id'],'id,presetname', + 'addressbooks', false, 'user_id'); + + foreach($abooks as $abook) { + $abookid = $abook['id']; + if( isset($_POST[$abookid."_cd_delete"]) ) { + self::delete_abook($abookid); + + } else { + $newset = array ( + 'name' => rcube_utils::get_input_value($abookid."_cd_name", rcube_utils::INPUT_POST), + 'username' => rcube_utils::get_input_value($abookid."_cd_username", rcube_utils::INPUT_POST, true), + 'url' => rcube_utils::get_input_value($abookid."_cd_url", rcube_utils::INPUT_POST), + 'active' => isset($_POST[$abookid.'_cd_active']) ? 1 : 0, + 'use_categories' => isset($_POST[$abookid.'_cd_use_categories']) ? 1 : 0, + 'refresh_time' => rcube_utils::get_input_value($abookid."_cd_refresh_time", rcube_utils::INPUT_POST), + ); + + // only set the password if the user entered a new one + $password = rcube_utils::get_input_value($abookid."_cd_password", rcube_utils::INPUT_POST, true); + if(strlen($password) > 0) { + $newset['password'] = $password; + } + + // remove admin only settings + foreach($newset as $pref => $value) { + if(self::no_override($pref, $abook, $prefs)) { + unset($newset[$pref]); + } + } + + self::update_abook($abookid, $newset); + } + } + + // add a new address book? + $new = rcube_utils::get_input_value('new_cd_name', rcube_utils::INPUT_POST); + if ( (!array_key_exists('_GLOBAL', $prefs) || !$prefs['_GLOBAL']['fixed']) && strlen($new) > 0) { + $srv = rcube_utils::get_input_value('new_cd_url', rcube_utils::INPUT_POST); + $usr = rcube_utils::get_input_value('new_cd_username', rcube_utils::INPUT_POST, true); + $pass = rcube_utils::get_input_value('new_cd_password', rcube_utils::INPUT_POST, true); + $pass = self::$helper->encrypt_password($pass); + $abname = rcube_utils::get_input_value('new_cd_name', rcube_utils::INPUT_POST); + $use_categories = intval(rcube_utils::get_input_value('new_cd_use_categories', rcube_utils::INPUT_POST, true), 0); + + $discovery = new carddav_discovery(); + $srvs = $discovery->find_addressbooks($srv, $usr, $pass); + + if(is_array($srvs) && count($srvs)>0) { + foreach($srvs as $srv){ + self::$helper->debug("ADDING ABOOK " . print_r($srv,true)); + $this_abname = $abname; + if($srv['name']) { + $this_abname .= ' (' . $srv['name'] . ')'; + } + self::insert_abook(array( + 'name' => $this_abname, + 'username' => $usr, + 'password' => $pass, + 'use_categories' => $use_categories, + 'url' => $srv['href'], + 'refresh_time' => rcube_utils::get_input_value('new_cd_refresh_time', rcube_utils::INPUT_POST) + )); + } + } else { + $args['abort'] = true; + $args['message'] = $abname . ': ' . $this->gettext('cd_err_noabfound'); + } + } + + return($args); + }}} + + private static function delete_abook($abookid) + {{{ + carddav_backend::delete_dbrecord($abookid,'addressbooks'); + // we explicitly delete all data belonging to the addressbook, since + // cascaded deleted are not supported by all database backends + // ...contacts + carddav_backend::delete_dbrecord($abookid,'contacts','abook_id'); + // ...custom subtypes + carddav_backend::delete_dbrecord($abookid,'xsubtypes','abook_id'); + // ...groups and memberships + $delgroups = carddav_backend::get_dbrecord($abookid, 'id as group_id', 'groups', false, 'abook_id'); + carddav_backend::delete_dbrecord($abookid,'groups','abook_id'); + carddav_backend::delete_dbrecord($delgroups,'group_user','group_id'); + }}} + + private static function insert_abook($pa) + {{{ + $dbh = rcmail::get_instance()->db; + + // check parameters + if(array_key_exists('refresh_time', $pa)) { + $pa['refresh_time'] = self::process_cd_time($pa['refresh_time']); + } + /* Ensure field lengths */ + if (array_key_exists('name', $pa)) { + if (strlen($pa['name']) > 64){ + $pa['name'] = substr($pa['name'], 0, 64); + } + } + if (array_key_exists('username', $pa)) { + if (strlen($pa['username']) > 255){ + $pa['username'] = substr($pa['username'], 0, 255); + } + } + if (array_key_exists('presetname', $pa)) { + if (strlen($pa['presetname']) > 255){ + $pa['presetname'] = substr($pa['presetname'], 0, 255); + } + } + $pa['user_id'] = $_SESSION['user_id']; + + // required fields + $qf=array('name','username','password','url','user_id'); + $qv=array(); + foreach($qf as $f) { + if(!array_key_exists($f,$pa)) return false; + $qv[] = $pa[$f]; + } + + // optional fields + $qfo = array('active','presetname','use_categories','refresh_time'); + foreach($qfo as $f) { + if(array_key_exists($f,$pa)) { + $qf[] = $f; + $qv[] = $pa[$f]; + } + } + + $dbh->query('INSERT INTO ' . $dbh->table_name('carddav_addressbooks') . + '('. implode(',',$qf) .') ' . + 'VALUES (?'. str_repeat(',?', count($qf)-1) . ')', + $qv + ); + }}} + + public static function update_abook($abookid, $pa) + {{{ + $dbh = rcmail::get_instance()->db; + + // check parameters + if(array_key_exists('refresh_time', $pa)) + $pa['refresh_time'] = self::process_cd_time($pa['refresh_time']); + + // encrypt the password before storing it + if(array_key_exists('password', $pa)) + $pa['password'] = self::$helper->encrypt_password($pa['password']); + + /* Ensure field lengths */ + if (array_key_exists('name', $pa)) { + if (strlen($pa['name']) > 64){ + $pa['name'] = substr($pa['name'], 0, 64); + } + } + if (array_key_exists('username', $pa)) { + if (strlen($pa['username']) > 255){ + $pa['username'] = substr($pa['username'], 0, 255); + } + } + if (array_key_exists('presetname', $pa)) { + if (strlen($pa['presetname']) > 255){ + $pa['presetname'] = substr($pa['presetname'], 0, 255); + } + } + + // optional fields + $qfo=array('name','username','password','url','active','refresh_time','sync_token'); + $qf=array(); + $qv=array(); + + foreach($qfo as $f) { + if(array_key_exists($f,$pa)) { + $qf[] = $f; + $qv[] = $pa[$f]; + } + } + if(count($qf) <= 0) return true; + + $qv[] = $abookid; + $dbh->query('UPDATE ' . + $dbh->table_name('carddav_addressbooks') . + ' SET ' . implode('=?,', $qf) . '=?' . + ' WHERE id=?', + $qv + ); + }}} + +} + +?> diff --git a/plugins/carddav/carddav_backend.php b/plugins/carddav/carddav_backend.php new file mode 100644 index 0000000..a26257d --- /dev/null +++ b/plugins/carddav/carddav_backend.php @@ -0,0 +1,2460 @@ +, + Michael Stilkerich + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +require_once("carddav_common.php"); + +use \Sabre\VObject; + +class carddav_backend extends rcube_addressbook +{ + private static $helper; + + // database primary key, used by RC to search by ID + public $primary_key = 'id'; + public $coltypes; + private $fallbacktypes = array( 'email' => array('internet') ); + + // database ID of the addressbook + private $id; + // currently active search filter + private $filter; + + private $result; + // configuration of the addressbook + private $config; + // The value of the global "sync_collection_workaround" preference. + // Defaults to false if the user comments it out. + private $sync_collection_workaround = false; + // custom labels defined in the addressbook + private $xlabels; + + const SEPARATOR = ','; + + // contains a the URIs, db ids and etags of the locally stored cards whenever + // a refresh from the server is attempted. This is used to avoid a separate + // "exists?" DB query for each card retrieved from the server and also allows + // to detect whether cards were deleted on the server + private $existing_card_cache = array(); + // same thing for groups + private $existing_grpcard_cache = array(); + // used in refresh DB to record group memberships for the delayed + // creation in the database (after all contacts have been loaded and + // stored from the server) + private $users_to_add; + + // total number of contacts in address book + private $total_cards = -1; + // attributes that are redundantly stored in the contact table and need + // not be parsed from the vcard + private $table_cols = array('id', 'name', 'email', 'firstname', 'surname'); + + // maps VCard property names to roundcube keys + private $vcf2rc = array( + 'simple' => array( + 'BDAY' => 'birthday', + 'FN' => 'name', + 'NICKNAME' => 'nickname', + 'NOTE' => 'notes', + 'PHOTO' => 'photo', + 'TITLE' => 'jobtitle', + 'UID' => 'cuid', + 'X-ABShowAs' => 'showas', + 'X-ANNIVERSARY' => 'anniversary', + 'X-ASSISTANT' => 'assistant', + 'X-GENDER' => 'gender', + 'X-MANAGER' => 'manager', + 'X-SPOUSE' => 'spouse', + // the two kind attributes should not occur both in the same vcard + //'KIND' => 'kind', // VCard v4 + 'X-ADDRESSBOOKSERVER-KIND' => 'kind', // Apple Addressbook extension + ), + 'multi' => array( + 'EMAIL' => 'email', + 'TEL' => 'phone', + 'URL' => 'website', + ), + ); + + // array with list of potential date fields for formatting + private $datefields = array('birthday', 'anniversary'); + + public function __construct($dbid) + {{{ + $dbh = rcmail::get_instance()->db; + + $this->ready = $dbh && !$dbh->is_error(); + $this->groups = true; + $this->readonly = false; + $this->id = $dbid; + + $this->config = self::carddavconfig($dbid); + + if ($this->config["needs_update"]){ + $this->refreshdb_from_server(); + } + + $prefs = carddav_common::get_adminsettings(); + if($this->config['presetname']) { + if($prefs[$this->config['presetname']]['readonly']) + $this->readonly = true; + } + + if (isset($prefs['_GLOBAL']['sync_collection_workaround'])) { + $this->sync_collection_workaround = + $prefs['_GLOBAL']['sync_collection_workaround']; + } + + $rc = rcmail::get_instance(); + $this->coltypes = array( /* {{{ */ + 'name' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('name'), 'category' => 'main'), + 'firstname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('firstname'), 'category' => 'main'), + 'surname' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('surname'), 'category' => 'main'), + 'email' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('email'), 'subtypes' => array('home','work','other','internet'), 'category' => 'main'), + 'middlename' => array('type' => 'text', 'size' => 19, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('middlename'), 'category' => 'main'), + 'prefix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $rc->gettext('nameprefix'), 'category' => 'main'), + 'suffix' => array('type' => 'text', 'size' => 8, 'maxlength' => 20, 'limit' => 1, 'label' => $rc->gettext('namesuffix'), 'category' => 'main'), + 'nickname' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('nickname'), 'category' => 'main'), + 'jobtitle' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('jobtitle'), 'category' => 'main'), + 'organization' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('organization'), 'category' => 'main'), + 'department' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('department'), 'category' => 'main'), + 'gender' => array('type' => 'select', 'limit' => 1, 'label' => $rc->gettext('gender'), 'options' => array('male' => $rc->gettext('male'), 'female' => $rc->gettext('female')), 'category' => 'personal'), + 'phone' => array('type' => 'text', 'size' => 40, 'maxlength' => 20, 'label' => $rc->gettext('phone'), 'subtypes' => array('home','home2','work','work2','mobile','cell','main','homefax','workfax','car','pager','video','assistant','other'), 'category' => 'main'), + 'address' => array('type' => 'composite', 'label' => $rc->gettext('address'), 'subtypes' => array('home','work','other'), 'childs' => array( + 'street' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('street'), 'category' => 'main'), + 'locality' => array('type' => 'text', 'size' => 28, 'maxlength' => 50, 'label' => $rc->gettext('locality'), 'category' => 'main'), + 'zipcode' => array('type' => 'text', 'size' => 8, 'maxlength' => 15, 'label' => $rc->gettext('zipcode'), 'category' => 'main'), + 'region' => array('type' => 'text', 'size' => 12, 'maxlength' => 50, 'label' => $rc->gettext('region'), 'category' => 'main'), + 'country' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('country'), 'category' => 'main'),), 'category' => 'main'), + 'birthday' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $rc->gettext('birthday'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'), + 'anniversary' => array('type' => 'date', 'size' => 12, 'maxlength' => 16, 'label' => $rc->gettext('anniversary'), 'limit' => 1, 'render_func' => 'rcmail_format_date_col', 'category' => 'personal'), + 'website' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'label' => $rc->gettext('website'), 'subtypes' => array('homepage','work','blog','profile','other'), 'category' => 'main'), + 'notes' => array('type' => 'textarea', 'size' => 40, 'rows' => 15, 'maxlength' => 500, 'label' => $rc->gettext('notes'), 'limit' => 1), + 'photo' => array('type' => 'image', 'limit' => 1, 'category' => 'main'), + 'assistant' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('assistant'), 'category' => 'personal'), + 'manager' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('manager'), 'category' => 'personal'), + 'spouse' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1, 'label' => $rc->gettext('spouse'), 'category' => 'personal'), + // TODO: define fields for vcards like GEO, KEY + ); /* }}} */ + $this->addextrasubtypes(); + }}} + + /** + * Stores a custom label in the database (X-ABLabel extension). + * + * @param string Name of the type/category (phone,address,email) + * @param string Name of the custom label to store for the type + */ + private function storeextrasubtype($typename, $subtype) + {{{ + $dbh = rcmail::get_instance()->db; + $sql_result = $dbh->query('INSERT INTO ' . + $dbh->table_name('carddav_xsubtypes') . + ' (typename,subtype,abook_id) VALUES (?,?,?)', + $typename, $subtype, $this->id); + }}} + + /** + * Adds known custom labels to the roundcube subtype list (X-ABLabel extension). + * + * Reads the previously seen custom labels from the database and adds them to the + * roundcube subtype list in #coltypes and additionally stores them in the #xlabels + * list. + */ + private function addextrasubtypes() + {{{ + $this->xlabels = array(); + + foreach($this->coltypes as $k => $v) { + if(array_key_exists('subtypes', $v)) { + $this->xlabels[$k] = array(); + } } + + // read extra subtypes + $xtypes = self::get_dbrecord($this->id,'typename,subtype','xsubtypes',false,'abook_id'); + + foreach ($xtypes as $row) { + $this->coltypes[$row['typename']]['subtypes'][] = $row['subtype']; + $this->xlabels[$row['typename']][] = $row['subtype']; + } + }}} + + /** + * Returns addressbook name (e.g. for addressbooks listing). + * + * @return string name of this addressbook + */ + public function get_name() + {{{ + return $this->config['name']; + }}} + + /** + * Save a search string for future listings. + * + * @param mixed Search params to use in listing method, obtained by get_search_set() + */ + public function set_search_set($filter) + {{{ + $this->filter = $filter; + $this->total_cards = -1; + }}} + + /** + * Getter for saved search properties + * + * @return mixed Search properties used by this class + */ + public function get_search_set() + {{{ + return $this->filter; + }}} + + /** + * Reset saved results and search parameters + */ + public function reset() + {{{ + $this->result = null; + $this->filter = null; + $this->total_cards = -1; + }}} + + /** + * Determines the name to be displayed for a contact. The routine + * distinguishes contact cards for individuals from organizations. + */ + private function set_displayname(&$save_data) + {{{ + if(strcasecmp($save_data['showas'], 'COMPANY') == 0 && strlen($save_data['organization'])>0) { + $save_data['name'] = $save_data['organization']; + } + + // we need a displayname; if we do not have one, try to make one up + if(strlen($save_data['name']) == 0) { + $dname = array(); + if(strlen($save_data['firstname'])>0) + $dname[] = $save_data['firstname']; + if(strlen($save_data['surname'])>0) + $dname[] = $save_data['surname']; + + if(count($dname) > 0) { + $save_data['name'] = implode(' ', $dname); + + } else { // no name? try email and phone + $ep_keys = array_keys($save_data); + $ep_keys = preg_grep(";^(email|phone):;", $ep_keys); + sort($ep_keys, SORT_STRING); + foreach($ep_keys as $ep_key) { + $ep_vals = $save_data[$ep_key]; + if(!is_array($ep_vals)) $ep_vals = array($ep_vals); + + foreach($ep_vals as $ep_val) { + if(strlen($ep_val)>0) { + $save_data['name'] = $ep_val; + break 2; + } + } + } + } + + // still no name? set to unknown and hope the user will fix it + if(strlen($save_data['name']) == 0) + $save_data['name'] = 'Unset Displayname'; + } + }}} + + /** + * Stores a group vcard in the database. + * + * @param string etag of the VCard in the given version on the CardDAV server + * @param string path to the VCard on the CardDAV server + * @param string string representation of the VCard + * @param array associative array containing at least name and cuid (card UID) + * @param int optionally, database id of the group if the store operation is an update + * + * @return int The database id of the created or updated card, false on error. + */ + private function dbstore_group($etag, $uri, $vcfstr, $save_data, $dbid=0) + {{{ + return $this->dbstore_base('groups',$etag,$uri,$vcfstr,$save_data,$dbid); + }}} + + private function dbstore_base($table, $etag, $uri, $vcfstr, $save_data, $dbid=0, $xcol=array(), $xval=array()) + {{{ + $dbh = rcmail::get_instance()->db; + + // get rid of the %u placeholder in the URI, otherwise the refresh operation + // will not be able to match local cards with those provided by the server + $username = $this->config['username']; + if($username === "%u") + $username = $_SESSION['username']; + $uri = str_replace("%u", $username, $uri); + + $xcol[]='name'; $xval[]=$save_data['name']; + $xcol[]='etag'; $xval[]=$etag; + $xcol[]='vcard'; $xval[]=$vcfstr; + + if($dbid) { + self::$helper->debug("UPDATE card $uri"); + $xval[]=$dbid; + $sql_result = $dbh->query('UPDATE ' . + $dbh->table_name("carddav_$table") . + ' SET ' . implode('=?,', $xcol) . '=?' . + ' WHERE id=?', $xval); + + } else { + self::$helper->debug("INSERT card $uri"); + if ("x".$save_data['cuid'] == "x"){ + // There is no contact UID in the VCARD, try to create one + $cuid = $uri; + $cuid = preg_replace(';^.*/;', "", $cuid); + $cuid = preg_replace(';\.vcf$;', "", $cuid); + $save_data['cuid'] = $cuid; + } + $xcol[]='abook_id'; $xval[]=$this->id; + $xcol[]='uri'; $xval[]=$uri; + $xcol[]='cuid'; $xval[]=$save_data['cuid']; + + $sql_result = $dbh->query('INSERT INTO ' . + $dbh->table_name("carddav_$table") . + ' (' . implode(',',$xcol) . ') VALUES (?' . str_repeat(',?', count($xcol)-1) .')', + $xval); + + $dbid = $dbh->insert_id("carddav_$table"); + } + + if($dbh->is_error()) { + self::$helper->warn($dbh->is_error()); + $this->set_error(self::ERROR_SAVING, $dbh->is_error()); + return false; + } + + return $dbid; + }}} + + /** + * Stores a contact to the local database. + * + * @param string etag of the VCard in the given version on the CardDAV server + * @param string path to the VCard on the CardDAV server + * @param string string representation of the VCard + * @param array associative array containing the roundcube save data for the contact + * @param int optionally, database id of the contact if the store operation is an update + * + * @return int The database id of the created or updated card, false on error. + */ + private function dbstore_contact($etag, $uri, $vcfstr, $save_data, $dbid=0) + {{{ + $this->preprocess_rc_savedata($save_data); + // build email search string + $email_keys = preg_grep('/^email(:|$)/', array_keys($save_data)); + $email_addrs = array(); + foreach($email_keys as $email_key) { + $email_addrs = array_merge($email_addrs, (array) $save_data[$email_key]); + } + $save_data['email'] = implode(', ', $email_addrs); + + // extra columns for the contacts table + $xcol_all=array('firstname','surname','organization','showas','email'); + $xcol=array(); + $xval=array(); + foreach($xcol_all as $k) { + if(array_key_exists($k,$save_data)) { + $xcol[] = $k; + $xval[] = $save_data[$k]; + } } + + return $this->dbstore_base('contacts',$etag,$uri,$vcfstr,$save_data,$dbid,$xcol,$xval); + }}} + + /** + * Checks if the given local card cache (for contacts or groups) contains + * a card with the given URI. If not, the function returns false. + * If yes, the card is marked seen in the cache, and the cached etag is + * compared with the given one. The function returns an associative array + * with the database id of the existing card (key dbid) and a boolean that + * indicates whether the card needs a server refresh as determined by the + * etag comparison (key needs_update). + */ + private static function checkcache(&$cache, $uri, $etag) + {{{ + if(!array_key_exists($uri, $cache)) + return false; + + $cache[$uri]['seen'] = true; + + $dbrec = $cache[$uri]; + $dbid = $dbrec['id']; + + $needsupd = true; + + // abort if card has not changed + if($etag === $dbrec['etag']) { + self::$helper->debug("UNCHANGED card $uri"); + $needsupd = false; + } + return array('needs_update'=>$needsupd, 'dbid'=>$dbid); + }}} + + /** + * Synchronizes the local card store with the CardDAV server. + */ + private function refreshdb_from_server() + {{{ + $dbh = rcmail::get_instance()->db; + $duration = time(); + + // determine existing local contact URIs and ETAGs + $contacts = self::get_dbrecord($this->id,'id,uri,etag','contacts',false,'abook_id'); + foreach($contacts as $contact) { + $this->existing_card_cache[$contact['uri']] = $contact; + } + + if(!$this->config['use_categories']) { + // determine existing local group URIs and ETAGs + $groups = self::get_dbrecord($this->id,'id,uri,etag','groups',false,'abook_id'); + foreach($groups as $group) { + $this->existing_grpcard_cache[$group['uri']] = $group; + } + } + + // used to record which users need to be added to which groups + $this->users_to_add = array(); + + // Check for supported-report-set and only use sync-collection if server advertises it. + // This suppresses 501 Not implemented errors with ownCloud. + $opts = array( + 'method'=>"PROPFIND", + 'header'=>array("Depth: 0", 'Content-Type: application/xml; charset="utf-8"'), + 'content'=> << + + + +EOF + ); + $reply = self::$helper->cdfopen($this->config['url'], $opts, $this->config); + + $records = -1; + + $xml = self::$helper->checkAndParseXML($reply); + if($xml !== false) { + $xpresult = $xml->xpath('//RCMCD:supported-report/RCMCD:report/RCMCD:sync-collection'); + // To avoid sync-collection, we can simply skip the next line + // leaving $records = -1 which will trigger a call to + // list_records_propfind() below. + if(count($xpresult) > 0 && !$this->sync_collection_workaround) { + $records = $this->list_records_sync_collection(); + } + } + + // sync-collection not supported or returned error + if ($records < 0){ + $records = $this->list_records_propfind(); + } + + foreach($this->users_to_add as $dbid => $cuids) { + if(count($cuids)<=0) continue; + $sql_result = $dbh->query('INSERT INTO '. + $dbh->table_name('carddav_group_user') . + ' (group_id,contact_id) SELECT ?,id from ' . + $dbh->table_name('carddav_contacts') . + ' WHERE abook_id=? AND cuid IN (' . implode(',', $cuids) . ')', $dbid, $this->id); + self::$helper->debug("Added " . $dbh->affected_rows($sql_result) . " contacts to group $dbid"); + } + + unset($this->users_to_add); + $this->existing_card_cache = array(); + $this->existing_grpcard_cache = array(); + + // set last_updated timestamp + $dbh->query('UPDATE ' . + $dbh->table_name('carddav_addressbooks') . + ' SET last_updated=' . $dbh->now() .' WHERE id=?', + $this->id); + + $duration = time() - $duration; + self::$helper->debug("server refresh took $duration seconds"); + if($records < 0) { + self::$helper->warn("Errors occurred during the refresh of addressbook " . $this->id); + } + }}} + + /** + * List the current set of contact records + * + * @param array List of cols to show, Null means all + * @param int Only return this number of records, use negative values for tail + * @param boolean True to skip the count query (select only) + * @return array Indexed list of contact records, each a hash array + */ + public function list_records($cols=array(), $subset=0, $nocount=false) + {{{ + // refresh from server if refresh interval passed + if ( $this->config['needs_update'] == 1 ) + $this->refreshdb_from_server(); + + // XXX workaround for a roundcube bug to support roundcube's displayname setting + // Reported as Roundcube Ticket #1488394 + if(count($cols)>0) { + if(!in_array('firstname', $cols)) { + $cols[] = 'firstname'; + } + if(!in_array('surname', $cols)) { + $cols[] = 'surname'; + } + } + // XXX workaround for a roundcube bug to support roundcube's displayname setting + + // if the count is not requested we can save one query + if($nocount) + $this->result = new rcube_result_set(); + else + $this->result = $this->count(); + + $records = $this->list_records_readdb($cols,$subset); + if($nocount) { + $this->result->count = $records; + + } else if ($this->list_page <= 1) { + if ($records < $this->page_size && $subset == 0) + $this->result->count = $records; + else + $this->result->count = $this->_count($cols); + } + + if ($records > 0){ + return $this->result; + } + + return false; + }}} + + /** + * Retrieves the Card URIs from the CardDAV server + * + * @return int number of cards in collection, -1 on error + */ + private function list_records_sync_collection() + {{{ + $sync_token = $this->config['sync_token']; + + while(true) { + $opts = array( + 'method'=>"REPORT", + 'header'=>array("Depth: 0", 'Content-Type: application/xml; charset="utf-8"'), + 'content'=> << + + $sync_token + 1 + + + + + +EOF + ); + + $reply = self::$helper->cdfopen($this->config['url'], $opts, $this->config); + + $xml = self::$helper->checkAndParseXML($reply); + + if($xml === false || (is_array($reply) && ($reply["status"] < 200 || $reply["status"] >= 300))) { + // a server may invalidate old sync-tokens, in which case we need to do a full resync + if (strlen($sync_token)>0 && ($reply == 412 || (is_array($reply) && $reply["status"] == 412))){ + self::$helper->warn("Server reported invalid sync-token in sync of addressbook " . $this->config['abookid'] . ". Resorting to full resync."); + $sync_token = ''; + continue; + } else { + $errorstatus = is_array($reply) ? $reply["status"] : $reply; + self::$helper->warn("An error (status " . $errorstatus . ") occured while retrieving the sync-token of addressbook " . $this->config['abookid'] . ". Sync-collection synchronization aborted. Will use propfind synchronization instead."); + return -1; + } + } + + list($new_sync_token) = $xml->xpath('//RCMCD:sync-token'); + + $records = $this->addvcards($xml); + + if(strlen($sync_token) == 0) { + if($records>=0) { + $this->delete_unseen(); + } + } else { + $this->delete_synccoll($xml); + } + + if($records >= 0) { + carddav::update_abook($this->config['abookid'], array('sync_token' => "$new_sync_token")); + + // if we got a truncated result set continue sync + $xpresult = $xml->xpath('//RCMCD:response[contains(child::RCMCD:status, " 507 Insufficient Storage")]'); + if(count($xpresult) > 0) { + $sync_token = "$new_sync_token"; + continue; + } + } + + break; + } + return $records; + }}} + + private function list_records_readdb($cols, $subset=0, $count_only=false) + {{{ + $dbh = rcmail::get_instance()->db; + + // true if we can use DB filtering or no filtering is requested + $filter = $this->get_search_set(); + $this->determine_filter_params($cols,$subset, $firstrow, $numrows, $read_vcard); + + $dbattr = $read_vcard ? 'vcard' : 'firstname,surname,email'; + + $limit_index = $firstrow; + $limit_rows = $numrows; + + $xfrom = ''; + $xwhere = ''; + if($this->group_id) { + $xfrom = ',' . $dbh->table_name('carddav_group_user'); + $xwhere = ' AND id=contact_id AND group_id=' . $dbh->quote($this->group_id) . ' '; + } + + if ($this->config['presetname']){ + $prefs = carddav_common::get_adminsettings(); + if (array_key_exists("require_always", $prefs[$this->config['presetname']])){ + foreach ($prefs[$this->config['presetname']]["require_always"] as $col){ + $xwhere .= " AND $col <> ".$dbh->quote('')." "; + } + } + } + + // Workaround for Roundcube versions < 0.7.2 + $sort_column = $this->sort_col ? $this->sort_col : 'surname'; + $sort_order = $this->sort_order ? $this->sort_order : 'ASC'; + + $sql_result = $dbh->limitquery("SELECT id,name,$dbattr FROM " . + $dbh->table_name('carddav_contacts') . $xfrom . + ' WHERE abook_id=? ' . $xwhere . + ($this->filter ? " AND (".$this->filter.")" : "") . + " ORDER BY (CASE WHEN showas='COMPANY' THEN organization ELSE " . $sort_column . " END) " + . $sort_order, + $limit_index, + $limit_rows, + $this->id + ); + + $addresses = array(); + while($contact = $dbh->fetch_assoc($sql_result)) { + if($read_vcard) { + $save_data = $this->create_save_data_from_vcard($contact['vcard']); + if (!$save_data){ + self::$helper->warn("Couldn't parse vcard ".$contact['vcard']); + continue; + } + + // needed by the calendar plugin + if(is_array($cols) && in_array('vcard', $cols)) { + $save_data['save_data']['vcard'] = $contact['vcard']; + } + + $save_data = $save_data['save_data']; + } else { + $save_data = array(); + foreach ($cols as $col) { + if(strcmp($col,'email')==0) + $save_data[$col] = preg_split('/,\s*/', $contact[$col]); + else + $save_data[$col] = $contact[$col]; + } + } + $addresses[] = array('ID' => $contact['id'], 'name' => $contact['name'], 'save_data' => $save_data); + } + + if(!$count_only) { + // create results for roundcube + foreach($addresses as $a) { + $a['save_data']['ID'] = $a['ID']; + $this->result->add($a['save_data']); + } + } + return count($addresses); + }}} + + private function query_addressbook_multiget($hrefs) + {{{ + $dbh = rcmail::get_instance()->db; + $hrefstr = ''; + foreach ($hrefs as $href) { + $hrefstr .= "$href\n"; + } + + $optsREPORT = array( + 'method'=>"REPORT", + 'header'=>array("Depth: 0", 'Content-Type: application/xml; charset="utf-8"'), + 'content'=> << + + + + + + + +$hrefstr + +EOF + ); + + $reply = self::$helper->cdfopen($this->config['url'], $optsREPORT, $this->config); + $xml = self::$helper->checkAndParseXML($reply); + if($xml === false || (is_array($reply) && ($reply["status"] < 200 || $reply["status"] >= 300))) { + $errorstatus = is_array($reply) ? $reply["status"] : $reply; + rcmail::write_log("carddav", "An error (status " . $errorstatus . ") occured while retrieving vcards for addressbook " . $this->config['abookid'] . ". Synchronization aborted."); + return -1; + } + + $xpresult = $xml->xpath('//RCMCD:response[descendant::RCMCC:address-data]'); + + $numcards = 0; + foreach ($xpresult as $vcard) { + self::$helper->registerNamespaces($vcard); + list($href) = $vcard->xpath('child::RCMCD:href'); + list($etag) = $vcard->xpath('descendant::RCMCD:getetag'); + list($vcf) = $vcard->xpath('descendant::RCMCC:address-data'); + + // determine database ID of existing cards by checking the cache + $dbid = 0; + if( ($ret = self::checkcache($this->existing_card_cache,"$href","$etag")) + || ($ret = self::checkcache($this->existing_grpcard_cache,"$href","$etag")) ) { + $dbid = $ret['dbid']; + } + + // changed on server, parse VCF + $save_data = $this->create_save_data_from_vcard("$vcf"); + $vcfobj = $save_data['vcf']; + if($save_data['needs_update']) + $vcf = $vcfobj->serialize(); + $save_data = $save_data['save_data']; + + if($save_data['kind'] === 'group') { + if(!$this->config['use_categories']) { + self::$helper->debug('Processing Group ' . $save_data['name']); + // delete current group members (will be reinserted if needed below) + if($dbid) self::delete_dbrecord($dbid,'group_user','group_id'); + + // store group card + if(!($dbid = $this->dbstore_group("$etag","$href","$vcf",$save_data,$dbid))) + return -1; + + // record group members for deferred store + $this->users_to_add[$dbid] = array(); + $members = $vcfobj->{'X-ADDRESSBOOKSERVER-MEMBER'}; + if ($members === null) { + $members = array(); + } + + self::$helper->debug("Group $dbid has " . count($members) . " members"); + foreach($members as $mbr) { + $mbr = preg_split('/:/', $mbr); + if(!$mbr) continue; + if(count($mbr)!=3 || $mbr[0] !== 'urn' || $mbr[1] !== 'uuid') { + self::$helper->warn("don't know how to interpret group membership: " . implode(':', $mbr)); + continue; + } + $this->users_to_add[$dbid][] = $dbh->quote($mbr[2]); + } + } + } else { // individual/other + if (trim($save_data['name']) == '') { // roundcube display fix for contacts that don't have first/last names + if ($save_data['nickname'] !== NULL && trim($save_data['nickname'] !== '')) { + $save_data['name'] = $save_data['nickname']; + } else { + foreach ($save_data as $key=>$val) { + if (strpos($key,'email') !== false) { + $save_data['name'] = $val[0]; + break; + } + } + } + } + if($this->config['use_categories']) { + // delete current member from groups (will be reinserted if needed below) + self::delete_dbrecord($dbid,'group_user','contact_id'); + foreach ($this->getCategories($vcfobj) as $category) { + if($category !== "All" && $category !== "Unfiled") { + $record = self::get_dbrecord($category, 'id', 'groups', true, 'name', array('abook_id' => $this->config['abookid'])); + if(!$record) { + $cuid = $this->find_free_uid(); + $uri = "$cuid.vcf"; + + $gsave_data = array( + 'name' => $category, + 'kind' => 'group', + 'cuid' => $cuid, + ); + $url = carddav_common::concaturl($this->config['url'], $uri); + $url = preg_replace(';https?://[^/]+;', '', $url); + // store group card + $vcfg = $this->create_vcard_from_save_data($gsave_data); + $vcfgstr = $vcfg->serialize(); + if(!($database = $this->dbstore_group("dummy",$url,$vcfgstr,$gsave_data))) + return -1; + } else { + $database = $record['id']; + } + if(!isset($this->users_to_add[$database])) { + $this->users_to_add[$database] = array(); + } + $uid = $save_data['cuid']; + $this->users_to_add[$database][] = $dbh->quote($uid); + } + } + } + if(!$this->dbstore_contact("$etag","$href","$vcf",$save_data,$dbid)) + return -1; + } + $numcards++; + } + return $numcards; + }}} + + private function list_records_propfind() + {{{ + $opts = array( + 'method'=>"PROPFIND", + 'header'=>array("Depth: 1", 'Content-Type: application/xml; charset="utf-8"'), + 'content'=> << + + + + +EOF + ); + + $reply = self::$helper->cdfopen("", $opts, $this->config); + $xml = self::$helper->checkAndParseXML($reply); + if($xml === false || (is_array($reply) && ($reply["status"] < 200 || $reply["status"] >= 300))) { + $errorstatus = is_array($reply) ? $reply["status"] : $reply; + rcmail::write_log("carddav", "An error (status " . $errorstatus . ") occured while retrieving the vcard list for addressbook " . $this->config['abookid'] . ". Synchronization aborted."); + return -1; + } + $records = $this->addvcards($xml); + if($records>=0) { + $this->delete_unseen(); + } + + return $records; + }}} + + private function addvcards($xml) + {{{ + $records = 0; + $urls = array(); + $xpresult = $xml->xpath('//RCMCD:response[starts-with(translate(child::RCMCD:propstat/RCMCD:status, "ABCDEFGHJIKLMNOPQRSTUVWXYZ", "abcdefghjiklmnopqrstuvwxyz"), "http/1.1 200 ") and child::RCMCD:propstat/RCMCD:prop/RCMCD:getetag]'); + foreach ($xpresult as $r) { + self::$helper->registerNamespaces($r); + + list($href) = $r->xpath('child::RCMCD:href'); + if(preg_match('/\/$/', $href)) continue; + + list($etag) = $r->xpath('descendant::RCMCD:getetag'); + + $ret = self::checkcache($this->existing_card_cache,"$href","$etag"); + $retgrp = self::checkcache($this->existing_grpcard_cache,"$href","$etag"); + + if( ($ret===false && $retgrp===false) + || (is_array($ret) && $ret['needs_update']) + || (is_array($retgrp) && $retgrp['needs_update']) ) + { + $urls[] = "$href"; + } + } + + if (count($urls) > 0) { + $records = $this->query_addressbook_multiget($urls); + } + + return $records; + }}} + + /** delete cards not present on the server anymore */ + private function delete_unseen() + {{{ + $delids = array(); + foreach($this->existing_card_cache as $value) { + if(!array_key_exists('seen', $value) || !$value['seen']) { + $delids[] = $value['id']; + } + } + $del = self::delete_dbrecord($delids); + self::$helper->debug("deleted $del contacts during server refresh"); + + $delids = array(); + foreach($this->existing_grpcard_cache as $value) { + if(!array_key_exists('seen', $value) || !$value['seen']) { + $delids[] = $value['id']; + } + } + $del = self::delete_dbrecord($delids,'groups'); + self::$helper->debug("deleted $del groups during server refresh"); + }}} + + /** delete cards reported deleted by the server */ + private function delete_synccoll($xml) + {{{ + $xpresult = $xml->xpath('//RCMCD:response[contains(child::RCMCD:status, " 404 Not Found")]'); + $del_contacts = array(); + $del_groups = array(); + + foreach ($xpresult as $r) { + self::$helper->registerNamespaces($r); + + list($href) = $r->xpath('child::RCMCD:href'); + if(preg_match('/\/$/', $href)) continue; + + if(isset($this->existing_card_cache["$href"])) { + $del_contacts[] = $this->existing_card_cache["$href"]['id']; + } else if(isset($this->existing_grpcard_cache["$href"])) { + $del_groups[] = $this->existing_grpcard_cache["$href"]['id']; + } + } + $del = self::delete_dbrecord($del_contacts); + self::$helper->debug("deleted $del contacts during incremental server refresh"); + $del = self::delete_dbrecord($del_groups,'groups'); + self::$helper->debug("deleted $del groups during incremental server refresh"); + }}} + + /** + * Search contacts + * + * @param mixed $fields The field name of array of field names to search in + * @param mixed $value Search value (or array of values when $fields is array) + * @param int $mode Matching mode: + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * @param boolean $select True if results are requested, False if count only + * @param boolean $nocount True to skip the count query (select only) + * @param array $required List of fields that cannot be empty + * + * @return object rcube_result_set Contact records and 'count' value + */ + function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array()) + {{{ + $dbh = rcmail::get_instance()->db; + if (!is_array($fields)) + $fields = array($fields); + if (!is_array($required) && !empty($required)) + $required = array($required); + + $where = $and_where = array(); + $mode = intval($mode); + $WS = ' '; + $AS = self::SEPARATOR; + + // build the $where array; each of its entries is an SQL search condition + foreach ($fields as $idx => $col) { + // direct ID search + if ($col == 'ID' || $col == $this->primary_key) { + $ids = !is_array($value) ? explode(self::SEPARATOR, $value) : $value; + $ids = $dbh->array2list($ids, 'integer'); + $where[] = $this->primary_key.' IN ('.$ids.')'; + continue; + } + + $val = is_array($value) ? $value[$idx] : $value; + // table column + if (in_array($col, $this->table_cols)) { + if ($mode & 1) { + // strict + $where[] = + // exact match 'name@domain.com' + '(' . $dbh->ilike($col, $val) + // line beginning match 'name@domain.com,%' + . ' OR ' . $dbh->ilike($col, $val . $AS . '%') + // middle match '%, name@domain.com,%' + . ' OR ' . $dbh->ilike($col, '%' . $AS . $WS . $val . $AS . '%') + // line end match '%, name@domain.com' + . ' OR ' . $dbh->ilike($col, '%' . $AS . $WS . $val) . ')'; + } elseif ($mode & 2) { + // prefix + $where[] = '(' . $dbh->ilike($col, $val . '%') + . ' OR ' . $dbh->ilike($col, $AS . $WS . $val . '%') . ')'; + } else { + // partial + $where[] = $dbh->ilike($col, '%' . $val . '%'); + } + } + // vCard field + else { + foreach (explode(" ", self::normalize_string($val)) as $word) { + if ($mode & 1) { + // strict + $words[] = '(' . $dbh->ilike('vcard', $word . $WS . '%') + . ' OR ' . $dbh->ilike('vcard', '%' . $AS . $WS . $word . $WS .'%') + . ' OR ' . $dbh->ilike('vcard', '%' . $AS . $WS . $word) . ')'; + } elseif ($mode & 2) { + // prefix + $words[] = '(' . $dbh->ilike('vcard', $word . '%') + . ' OR ' . $dbh->ilike('vcard', $AS . $WS . $word . '%') . ')'; + } else { + // partial + $words[] = $dbh->ilike('vcard', '%' . $word . '%'); + } + } + $where[] = '(' . join(' AND ', $words) . ')'; + if (is_array($value)) + $post_search[$col] = mb_strtolower($val); + } + } + + if ($this->config['presetname']){ + $prefs = carddav_common::get_adminsettings(); + if (array_key_exists("require_always", $prefs[$this->config['presetname']])){ + $required = array_merge($prefs[$this->config['presetname']]["require_always"], $required); + } + } + + foreach (array_intersect($required, $this->table_cols) as $col) { + $and_where[] = $dbh->quoteIdentifier($col).' <> '.$dbh->quote(''); + } + + if (!empty($where)) { + // use AND operator for advanced searches + $where = join(is_array($value) ? ' AND ' : ' OR ', $where); + } + + if (!empty($and_where)) + $where = ($where ? "($where) AND " : '') . join(' AND ', $and_where); + + // Post-searching in vCard data fields + // we will search in all records and then build a where clause for their IDs + if (!empty($post_search)) { + $ids = array(0); + // build key name regexp + $regexp = '/^(' . implode(array_keys($post_search), '|') . ')(?:.*)$/'; + // use initial WHERE clause, to limit records number if possible + if (!empty($where)) + $this->set_search_set($where); + + // count result pages + $cnt = $this->count(); + $pages = ceil($cnt / $this->page_size); + $scnt = count($post_search); + + // get (paged) result + for ($i=0; $i<$pages; $i++) { + $this->list_records(null, $i, true); + while ($row = $this->result->next()) { + $id = $row[$this->primary_key]; + $found = array(); + foreach (preg_grep($regexp, array_keys($row)) as $col) { + $pos = strpos($col, ':'); + $colname = $pos ? substr($col, 0, $pos) : $col; + $search = $post_search[$colname]; + foreach ((array)$row[$col] as $value) { + // composite field, e.g. address + foreach ((array)$value as $val) { + $val = mb_strtolower($val); + if ($mode & 1) { + $got = ($val == $search); + } elseif ($mode & 2) { + $got = ($search == substr($val, 0, strlen($search))); + } else { + $got = (strpos($val, $search) !== false); + } + + if ($got) { + $found[$colname] = true; + break 2; + } + } + } + } + // all fields match + if (count($found) >= $scnt) { + $ids[] = $id; + } + } + } + + // build WHERE clause + $ids = $dbh->array2list($ids, 'integer'); + $where = $this->primary_key.' IN ('.$ids.')'; + + // when we know we have an empty result + if ($ids == '0') { + $this->set_search_set($where); + return ($this->result = new rcube_result_set(0, 0)); + } + } + + if (!empty($where)) { + $this->set_search_set($where); + if ($select) + $this->list_records(null, 0, $nocount); + else + $this->result = $this->count(); + } + + return $this->result; + }}} + + /** + * Count number of available contacts in database + * + * @return rcube_result_set Result set with values for 'count' and 'first' + */ + public function count() + {{{ + if($this->total_cards < 0) { + $this->_count(); + } + return new rcube_result_set($this->total_cards, ($this->list_page-1) * $this->page_size); + }}} + + // Determines and returns the number of cards matching the current search criteria + private function _count($cols=array()) + {{{ + if($this->total_cards < 0) { + $dbh = rcmail::get_instance()->db; + + $sql_result = $dbh->query('SELECT COUNT(id) as total_cards FROM ' . + $dbh->table_name('carddav_contacts') . + ' WHERE abook_id=?' . + ($this->filter ? " AND (".$this->filter.")" : ""), + $this->id + ); + + $resultrow = $dbh->fetch_assoc($sql_result); + $this->total_cards = $resultrow['total_cards']; + } + return $this->total_cards; + }}} + + private function determine_filter_params($cols, $subset, &$firstrow, &$numrows, &$read_vcard) + {{{ + // determine whether we have to parse the vcard or if only db cols are requested + $read_vcard = !$cols || count(array_intersect($cols, $this->table_cols)) < count($cols); + + // determine result subset needed + $firstrow = ($subset>=0) ? + $this->result->first : ($this->result->first+$this->page_size+$subset); + $numrows = $subset ? abs($subset) : $this->page_size; + }}} + + /** + * Return the last result set + * + * @return rcube_result_set Current result set or NULL if nothing selected yet + */ + public function get_result() + {{{ + return $this->result; + }}} + + /** + * Return the last result set + * + * @return rcube_result_set Current result set or NULL if nothing selected yet + */ + private function get_record_from_carddav($uid) + {{{ + $opts = array( 'method'=>"GET" ); + $reply = self::$helper->cdfopen($uid, $opts, $this->config); + if (!is_array($reply) || strlen($reply["body"])==0) { return false; } + if ($reply["status"] == 404){ + self::$helper->warn("Request for VCF '$uid' which doesn't exist on the server."); + return false; + } + + return array( + 'vcf' => $reply["body"], + 'etag' => $reply['headers']['etag'], + ); + }}} + + /** + * Get a specific contact record + * + * @param mixed record identifier(s) + * @param boolean True to return record as associative array, otherwise a result set is returned + * + * @return mixed Result object with all record fields or False if not found + */ + public function get_record($oid, $assoc_return=false) + {{{ + $this->result = $this->count(); + + $contact = self::get_dbrecord($oid, 'vcard'); + if(!$contact) return false; + + $retval = $this->create_save_data_from_vcard($contact['vcard']); + if(!$retval) { + return false; + } + $vcfobj = $retval['vcf']; + $retval = $retval['save_data']; + $retval['__vcf'] = $vcfobj; + + $retval['ID'] = $oid; + $this->result->add($retval); + $sql_arr = $assoc_return && $this->result ? $this->result->first() : null; + return $assoc_return && $sql_arr ? $sql_arr : $this->result; + }}} + + private function put_record_to_carddav($id, $vcf, $etag='') + {{{ + $this->result = $this->count(); + $matchhdr = $etag ? + "If-Match: $etag" : + "If-None-Match: *"; + + $opts = array( + 'method'=>"PUT", + 'content'=>$vcf, + 'header'=> array( + "Content-Type: text/vcard; charset=\"utf-8\"", + $matchhdr, + ), + ); + $reply = self::$helper->cdfopen($id, $opts, $this->config); + if (is_array($reply) && $reply["status"] >= 200 && $reply["status"] < 300) { + $etag = $reply["headers"]["etag"]; + if ("$etag" == ""){ + // Server did not reply an etag + $retval = $this->get_record_from_carddav($id); + self::$helper->debug(var_export($retval, true)); + $etag = $retval["etag"]; + } + return $etag; + } + + return false; + }}} + + private function delete_record_from_carddav($id) + {{{ + $this->result = $this->count(); + $opts = array( 'method'=>"DELETE" ); + $reply = self::$helper->cdfopen($id, $opts, $this->config); + if (is_array($reply) && ($reply["status"] == 204 || $reply["status"] == 200)){ + return true; + } + return false; + }}} + + private function guid() + {{{ + return sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(16384, 20479), mt_rand(32768, 49151), mt_rand(0, 65535), mt_rand(0, 65535), mt_rand(0, 65535)); + }}} + + /** + * Creates a new or updates an existing vcard from save data. + */ + private function create_vcard_from_save_data($save_data, $vcf=null) + {{{ + unset($save_data['vcard']); + if(!$vcf) { // create fresh minimal vcard + $vcf = new VObject\Component\VCard( + array( + 'UID' => $save_data['cuid'], + 'REV' => gmdate("Y-m-d\TH:i:s\Z") + ) + ); + } else { // update revision + $vcf->REV = gmdate("Y-m-d\TH:i:s\Z"); + } + + // N is mandatory + if(array_key_exists('kind',$save_data) && $save_data['kind'] === 'group') { + $vcf->N = $save_data['name']; + } else { + $vcf->N = array( + $save_data['surname'], + $save_data['firstname'], + $save_data['middlename'], + $save_data['prefix'], + $save_data['suffix'], + ); + } + + $new_org_value = array(); + if (array_key_exists("organization", $save_data) && + strlen($save_data['organization']) > 0 ){ + $new_org_value[] = $save_data['organization']; + } + + if (array_key_exists("department", $save_data)){ + if (is_array($save_data['department'])){ + foreach ($save_data['department'] as $key => $value) { + $new_org_value[] = $value; + } + } else if (strlen($save_data['department']) > 0){ + $new_org_value[] = $save_data['department']; + } + } + + if (count($new_org_value) > 0) { + $vcf->ORG = $new_org_value; + } else { + unset($vcf->ORG); + } + + // normalize date fields to RFC2425 YYYY-MM-DD date values + foreach ($this->datefields as $key) { + if (array_key_exists($key, $save_data)) { + $data = (is_array($save_data[$key])) ? $save_data[$key][0] : $save_data[$key]; + if (strlen($data) > 0) { + $val = rcube_utils::strtotime($data); + $save_data[$key] = date('Y-m-d',$val); + } + } + } + + // due to a bug in earlier versions of RCMCardDAV the PHOTO field was encoded base64 TWICE + // This was recognized and fixed on 2013-01-09 and should be kept here until reasonable + // certain that it's been fixed on users data, too. + if (!array_key_exists('photo', $save_data) && strlen($vcf->PHOTO) > 0){ + $save_data['photo']= $vcf->PHOTO; + } + if (array_key_exists('photo', $save_data) && strlen($save_data['photo']) > 0 && base64_decode($save_data['photo'], true) !== FALSE){ + self::$helper->debug("photo is base64 encoded. Decoding..."); + $i=0; + while(base64_decode($save_data['photo'], true)!==FALSE && $i++ < 10){ + self::$helper->debug("Decoding $i..."); + $save_data['photo'] = base64_decode($save_data['photo'], true); + } + if ($i >= 10){ + lef::$helper->warn("PHOTO of ".$save_data['uid']." does not decode after 10 attempts..."); + } + } + + // process all simple attributes + foreach ($this->vcf2rc['simple'] as $vkey => $rckey){ + if (array_key_exists($rckey, $save_data)) { + $data = (is_array($save_data[$rckey])) ? $save_data[$rckey][0] : $save_data[$rckey]; + if (strlen($data) > 0) { + $vcf->{$vkey} = $data; + } else { // delete the field + unset($vcf->{$vkey}); + } + } + } + + // Special handling for PHOTO + if ($property = $vcf->PHOTO) { + $property['ENCODING'] = 'B'; + $property['VALUE'] = 'BINARY'; + } + + // process all multi-value attributes + foreach ($this->vcf2rc['multi'] as $vkey => $rckey){ + // delete and fully recreate all entries + // there is no easy way of mapping an address in the existing card + // to an address in the save data, as subtypes may have changed + unset($vcf->{$vkey}); + + $stmap = array( $rckey => 'other' ); + foreach ($this->coltypes[$rckey]['subtypes'] AS $subtype){ + $stmap[ $rckey.':'.$subtype ] = $subtype; + } + + foreach ($stmap as $rcqkey => $subtype){ + if(array_key_exists($rcqkey, $save_data)) { + $avalues = is_array($save_data[$rcqkey]) ? $save_data[$rcqkey] : array($save_data[$rcqkey]); + foreach($avalues as $evalue) { + if (strlen($evalue) > 0){ + $prop = $vcf->add($vkey, $evalue); + $this->set_attr_label($vcf, $prop, $rckey, $subtype); // set label + } + }} + } + } + + // process address entries + unset($vcf->ADR); + foreach ($this->coltypes['address']['subtypes'] AS $subtype){ + $rcqkey = 'address:'.$subtype; + + if(array_key_exists($rcqkey, $save_data)) { + foreach($save_data[$rcqkey] as $avalue) { + if ( strlen($avalue['street']) + || strlen($avalue['locality']) + || strlen($avalue['region']) + || strlen($avalue['zipcode']) + || strlen($avalue['country'])) { + + $prop = $vcf->add('ADR', array( + '', + '', + $avalue['street'], + $avalue['locality'], + $avalue['region'], + $avalue['zipcode'], + $avalue['country'], + )); + $this->set_attr_label($vcf, $prop, 'address', $subtype); // set label + } + } } + } + + return $vcf; + }}} + + private function set_attr_label($vcard, $pvalue, $attrname, $newlabel) + {{{ + $group = $pvalue->group; + + // X-ABLabel? + if(in_array($newlabel, $this->xlabels[$attrname])) { + if(!$group) { + do { + $group = $this->guid(); + } while (null !== $vcard->{$group . '.X-ABLabel'}); + + $pvalue->group = $group; + + // delete standard label if we had one + $oldlabel = $pvalue['TYPE']; + if(strlen($oldlabel)>0 && + in_array($oldlabel, $this->coltypes[$attrname]['subtypes'])) { + unset($pvalue['TYPE']); + } + } + + $vcard->{$group . '.X-ABLabel'} = $newlabel; + return true; + } + + // Standard Label + $had_xlabel = false; + if($group) { // delete group label property if present + $had_xlabel = isset($vcard->{$group . '.X-ABLabel'}); + unset($vcard->{$group . '.X-ABLabel'}); + } + + // add or replace? + $oldlabel = $pvalue['TYPE']; + if(strlen($oldlabel)>0 && + in_array($oldlabel, $this->coltypes[$attrname]['subtypes'])) { + $had_xlabel = false; // replace + } + + if($had_xlabel &&is_array($pvalue['TYPE'])) { + $new_type = $pvalue['TYPE']; + array_unshift($new_type, $newlabel); + } else { + $new_type = $newlabel; + } + $pvalue['TYPE'] = $new_type; + + return false; + }}} + + private function get_attr_label($vcard, $pvalue, $attrname) + {{{ + // prefer a known standard label if available + $xlabel = ''; + $fallback = null; + + if(isset($pvalue['TYPE'])) { + foreach($pvalue['TYPE'] as $type) + { + $type = strtolower($type); + if(is_array($this->coltypes[$attrname]['subtypes']) && in_array($type, $this->coltypes[$attrname]['subtypes']) ) + { + $fallback = $type; + if(!(is_array($this->fallbacktypes[$attrname]) + && in_array($type, $this->fallbacktypes[$attrname]))) + { + return $type; + } + } + } + } + + if($fallback) { return $fallback; } + + // check for a custom label using Apple's X-ABLabel extension + $group = $pvalue->group; + if($group) { + $xlabel = $vcard->{$group . '.X-ABLabel'}; + if($xlabel) { + $xlabel = $xlabel->getParts(); + if($xlabel) + $xlabel = $xlabel[0]; + } + + // strange Apple label that I don't know to interpret + if(strlen($xlabel)<=0) { + return 'other'; + } + + if(preg_match(';_\$!<(.*)>!\$_;', $xlabel, $matches)) { + $match = strtolower($matches[1]); + if(in_array($match, $this->coltypes[$attrname]['subtypes'])) + return $match; + return 'other'; + } + + // add to known types if new + if(!in_array($xlabel, $this->coltypes[$attrname]['subtypes'])) { + $this->storeextrasubtype($attrname, $xlabel); + $this->coltypes[$attrname]['subtypes'][] = $xlabel; + } + return $xlabel; + } + + return 'other'; + }}} + + private function download_photo(&$save_data) + {{{ + $opts = array( 'method'=>"GET" ); + $uri = $save_data['photo']; + $reply = self::$helper->cdfopen($uri, $opts, $this->config); + if (is_array($reply) && $reply["status"] == 200){ + $save_data['photo'] = $reply['body']; + return true; + } + self::$helper->warn("Downloading $uri failed: " . (is_array($reply) ? $reply["status"] : $reply) ); + return false; + }}} + + /** + * Creates the roundcube representation of a contact from a VCard. + * + * If the card contains a URI referencing an external photo, this + * function will download the photo and inline it into the VCard. + * The returned array contains a boolean that indicates that the + * VCard was modified and should be stored to avoid repeated + * redownloads of the photo in the future. The returned VCard + * object contains the modified representation and can be used + * for storage. + * + * @param string Textual representation of a VCard. + * @return mixed false on failure, otherwise associative array with keys: + * - save_data: Roundcube representation of the VCard + * - vcf: VCard object created from the given VCard + * - needs_update: boolean that indicates whether the card was modified + */ + private function create_save_data_from_vcard($vcfstr) + {{{ + try { + $vcf = VObject\Reader::read($vcfstr, VObject\Reader::OPTION_FORGIVING); + } catch (Exception $e) { + self::$helper->warn("Couldn't parse vcard: $vcfstr"); + return false; + } + + $needs_update=false; + $save_data = array( + // DEFAULTS + 'kind' => 'individual', + ); + + foreach ($this->vcf2rc['simple'] as $vkey => $rckey){ + $property = $vcf->{$vkey}; + if ($property !== null){ + $p = $property->getParts(); + $save_data[$rckey] = $p[0]; + } + } + + // inline photo if external reference + if(array_key_exists('photo', $save_data)) { + $kind = $vcf->PHOTO['VALUE']; + if($kind && strcasecmp('uri', $kind)==0) { + if($this->download_photo($save_data)) { + unset($vcf->PHOTO['VALUE']); + $vcf->PHOTO['ENCODING'] = 'b'; + $vcf->PHOTO = $save_data['photo']; + $needs_update=true; + } + } + self::xabcropphoto($vcf, $save_data); + } + + $property = $vcf->N; + if ($property !== null){ + $N = $property->getParts(); + switch(count($N)){ + case 5: + $save_data['suffix'] = $N[4]; + case 4: + $save_data['prefix'] = $N[3]; + case 3: + $save_data['middlename'] = $N[2]; + case 2: + $save_data['firstname'] = $N[1]; + case 1: + $save_data['surname'] = $N[0]; + } + } + + $property = $vcf->ORG; + if ($property){ + $ORG = $property->getParts(); + $save_data['organization'] = $ORG[0]; + for ($i = 1; $i <= count($ORG); $i++){ + $save_data['department'][] = $ORG[$i]; + } + } + + foreach ($this->vcf2rc['multi'] as $key => $value){ + $property = $vcf->{$key}; + if ($property !== null) { + foreach ($property as $property_instance){ + $p = $property_instance->getParts(); + $label = $this->get_attr_label($vcf, $property_instance, $value); + $save_data[$value.':'.$label][] = $p[0]; + } + } + } + + $property = ($vcf->ADR) ? $vcf->ADR : array(); + foreach ($property as $property_instance){ + $p = $property_instance->getParts(); + $label = $this->get_attr_label($vcf, $property_instance, 'address'); + $adr = array( + 'pobox' => $p[0], // post office box + 'extended' => $p[1], // extended address + 'street' => $p[2], // street address + 'locality' => $p[3], // locality (e.g., city) + 'region' => $p[4], // region (e.g., state or province) + 'zipcode' => $p[5], // postal code + 'country' => $p[6], // country name + ); + $save_data['address:'.$label][] = $adr; + } + + // set displayname according to settings + $this->set_displayname($save_data); + + return array( + 'save_data' => $save_data, + 'vcf' => $vcf, + 'needs_update' => $needs_update, + ); + }}} + + + const MAX_PHOTO_SIZE = 256; + + public function xabcropphoto($vcard, &$save_data) + {{{ + if (!function_exists('gd_info') || $vcard == null) { + return $vcard; + } + $photo = $vcard->PHOTO; + if ($photo == null) { + return $vcard; + } + $abcrop = $vcard['X-ABCROP-RECTANGLE']; + if ($abcrop == null) { + return $vcard; + } + + $parts = explode('&', $abcrop); + $x = intval($parts[1]); + $y = intval($parts[2]); + $w = intval($parts[3]); + $h = intval($parts[4]); + $dw = min($w, self::MAX_PHOTO_SIZE); + $dh = min($h, self::MAX_PHOTO_SIZE); + + $src = imagecreatefromstring($photo); + $dst = imagecreatetruecolor($dw, $dh); + imagecopyresampled($dst, $src, 0, 0, $x, imagesy($src) - $y - $h, $dw, $dh, $w, $h); + + ob_start(); + imagepng($dst); + $data = ob_get_contents(); + ob_end_clean(); + $save_data['photo'] = $data; + + return $vcard; + }}} + private function find_free_uid() + {{{ + // find an unused UID + $cuid = $this->guid(); + while ($this->get_record_from_carddav("$cuid.vcf")){ + $cuid = $this->guid(); + } + return $cuid; + }}} + + /** + * Create a new contact record + * + * @param array Assoziative array with save data + * Keys: Field name with optional section in the form FIELD:SECTION + * Values: Field value. Can be either a string or an array of strings for multiple values + * @param boolean True to check for duplicates first + * @return mixed The created record ID on success, False on error + */ + public function insert($save_data, $check=false) + {{{ + $this->preprocess_rc_savedata($save_data); + + // find an unused UID + $save_data['cuid'] = $this->find_free_uid(); + + $vcf = $this->create_vcard_from_save_data($save_data); + if(!$vcf) return false; + $vcfstr = $vcf->serialize(); + + $uri = $save_data['cuid'] . '.vcf'; + if(!($etag = $this->put_record_to_carddav($uri, $vcfstr))) + return false; + + $url = carddav_common::concaturl($this->config['url'], $uri); + $url = preg_replace(';https?://[^/]+;', '', $url); + $dbid = $this->dbstore_contact($etag,$url,$vcfstr,$save_data); + if(!$dbid) return false; + + # Done by save.inc + #if ($this->groupd != -1) + # $this->add_to_group($this->group_id, $dbid); + + if($this->total_cards != -1) + $this->total_cards++; + return $dbid; + }}} + + /** + * Does some common preprocessing with save data created by roundcube. + */ + private function preprocess_rc_savedata(&$save_data) + {{{ + // heuristic to determine X-ABShowAs setting + // organization set but neither first nor surname => showas company + if(!$save_data['surname'] && !$save_data['firstname'] + && $save_data['organization'] && !array_key_exists('showas',$save_data)) { + $save_data['showas'] = 'COMPANY'; + } + if(!array_key_exists('showas',$save_data)) { + $save_data['showas'] = 'INDIVIDUAL'; + } + // organization not set but showas==company => show as regular + if(!$save_data['organization'] && $save_data['showas']==='COMPANY') { + $save_data['showas'] = 'INDIVIDUAL'; + } + + // generate display name according to display order setting + $this->set_displayname($save_data); + }}} + + /** + * Update a specific contact record + * + * @param mixed Record identifier + * @param array Assoziative array with save data + * Keys: Field name with optional section in the form FIELD:SECTION + * Values: Field value. Can be either a string or an array of strings for multiple values + * @return boolean True on success, False on error + */ + public function update($id, $save_data) + {{{ + // get current DB data + $contact = self::get_dbrecord($id,'id,cuid,uri,etag,vcard,showas'); + if(!$contact) return false; + + // complete save_data + $save_data['showas'] = $contact['showas']; + $this->preprocess_rc_savedata($save_data); + + // create vcard from current DB data to be updated with the new data + try { + $vcf = VObject\Reader::read($contact['vcard'], VObject\Reader::OPTION_FORGIVING); + } catch (Exception $e) { + self::$helper->warn("Update: Couldn't parse local vcard: ".$contact['vcard']); + return false; + } + + $vcf = $this->create_vcard_from_save_data($save_data, $vcf); + if(!$vcf) { + self::$helper->warn("Update: Couldn't adopt local vcard to new settings"); + return false; + } + + $vcfstr = $vcf->serialize(); + if(!($etag=$this->put_record_to_carddav($contact['uri'], $vcfstr, $contact['etag']))) { + self::$helper->warn("Updating card on server failed"); + return false; + } + $id = $this->dbstore_contact($etag,$contact['uri'],$vcfstr,$save_data,$id); + return ($id!=0); + }}} + + /** + * Mark one or more contact records as deleted + * + * @param array Record identifiers + * @param bool Remove records irreversible (see self::undelete) + */ + public function delete($ids, $force = true) + {{{ + $deleted = 0; + foreach ($ids as $dbid) { + $contact = self::get_dbrecord($dbid,'uri'); + if(!$contact) continue; + + // delete contact from all groups it is contained in + $groups = $this->get_record_groups($dbid); + foreach($groups as $group_id => $grpname) + $this->remove_from_group($group_id, $dbid); + + if($this->delete_record_from_carddav($contact['uri'])) { + $deleted += self::delete_dbrecord($dbid); + } + } + + if($this->total_cards != -1) + $this->total_cards -= $deleted; + return $deleted; + }}} + + private function update_contact_categories($id,$vcf) { + $groups = $this->get_record_groups($id); + + if($vcf->{'CATEGORY'}) { + $cat_name = "CATEGORY"; + } else { + $cat_name = "CATEGORIES"; + } + unset($vcf->{$cat_name}); + $categories = array(); + foreach($groups as $group_id => $grpname) { + $categories[] = $grpname; + } + $vcf->{$cat_name} = $categories; + } + + private function update_contacts($ids) { + foreach ($ids as $id) { + $contact = self::get_dbrecord($id,'id,cuid,uri,etag,vcard,showas'); + if(!$contact) return false; + + try { + $vcf = VObject\Reader::read($contact['vcard'], VObject\Reader::OPTION_FORGIVING); + } catch (Exception $e) { + self::$helper->warn("Update: Couldn't parse local vcard: ".$contact['vcard']); + return false; + } + + $this->update_contact_categories($id,$vcf); + + $vcfstr = $vcf->serialize(); + + $save_data_arr = $this->create_save_data_from_vcard("$vcfstr"); + $save_data = $save_data_arr['save_data']; + + // complete save_data + $save_data['showas'] = $contact['showas']; + $this->preprocess_rc_savedata($save_data); + + if(!($etag=$this->put_record_to_carddav($contact['uri'], $vcfstr, $contact['etag']))) { + self::$helper->warn("Updating card on server failed"); + return false; + } + $id = $this->dbstore_contact($etag,$contact['uri'],$vcfstr,$save_data,$id); + + } + return true; + } + + /** + * Add the given contact records the a certain group + * + * @param string Group identifier + * @param array List of contact identifiers to be added + * @return int Number of contacts added + */ + public function add_to_group($group_id, $ids) + {{{ + if (!is_array($ids)) { + $ids = explode(',', $ids); + } + + if(!$this->config['use_categories']) { + // get current DB data + $group = self::get_dbrecord($group_id,'uri,etag,vcard,name,cuid','groups'); + if(!$group) return false; + + // get current DB data + $group = self::get_dbrecord($group_id,'uri,etag,vcard,name,cuid','groups'); + if(!$group) return false; + + // create vcard from current DB data to be updated with the new data + try { + $vcf = VObject\Reader::read($group['vcard'], VObject\Reader::OPTION_FORGIVING); + } catch (Exception $e) { + self::$helper->warn("Update: Couldn't parse local group vcard: ".$group['vcard']); + return false; + } + + foreach ($ids as $cid) { + $contact = self::get_dbrecord($cid,'cuid'); + if(!$contact) return false; + + $vcf->add('X-ADDRESSBOOKSERVER-MEMBER', "urn:uuid:" . $contact['cuid']); + } + + $vcfstr = $vcf->serialize(); + if(!($etag = $this->put_record_to_carddav($group['uri'], $vcfstr, $group['etag']))) + return false; + + if(!$this->dbstore_group($etag,$group['uri'],$vcfstr,$group,$group_id)) + return false; + } + + $dbh = rcmail::get_instance()->db; + foreach ($ids as $cid) { + $dbh->query('INSERT INTO ' . + $dbh->table_name('carddav_group_user') . + ' (group_id,contact_id) VALUES (?,?)', + $group_id, $cid); + } + + if($this->config['use_categories']) { + if(!$this->update_contacts($ids)) + return false; + $added = count($ids); + } + return $added; + }}} + + /** + * Remove the given contact records from a certain group + * + * @param string Group identifier + * @param array List of contact identifiers to be removed + * @return int Number of deleted group members + */ + public function remove_from_group($group_id, $ids) + {{{ + if (!is_array($ids)) + $ids = explode(',', $ids); + if(!$this->config['use_categories']) { + // get current DB data + $group = self::get_dbrecord($group_id,'name,cuid,uri,etag,vcard','groups'); + if(!$group) return false; + + // create vcard from current DB data to be updated with the new data + try { + $vcf = VObject\Reader::read($group['vcard'], VObject\Reader::OPTION_FORGIVING); + } catch (Exception $e) { + self::$helper->warn("Update: Couldn't parse local group vcard: ".$group['vcard']); + return false; + } + + $deleted = 0; + foreach ($ids as $cid) { + $contact = self::get_dbrecord($cid,'cuid'); + if(!$contact) return false; + + $search_for = 'urn:uuid:' . $contact['cuid']; + foreach ($vcf->{'X-ADDRESSBOOKSERVER-MEMBER'} as $member) { + if ($member == $search_for) { + $vcf->remove($member); + break; + } + } + $deleted++; + } + + $vcfstr = $vcf->serialize(); + if(!($etag = $this->put_record_to_carddav($group['uri'], $vcfstr, $group['etag']))) + return false; + if(!$this->dbstore_group($etag,$group['uri'],$vcfstr,$group,$group_id)) + return false; + } + + $deleted = self::delete_dbrecord($ids,'group_user','contact_id', array('group_id' => $group_id)); + return $deleted; + }}} + + /** + * Get group assignments of a specific contact record + * + * @param mixed Record identifier + * + * @return array List of assigned groups as ID=>Name pairs + * @since 0.5-beta + */ + public function get_record_groups($id) + {{{ + $dbh = rcmail::get_instance()->db; + $sql_result = $dbh->query('SELECT id,name FROM '. + $dbh->table_name('carddav_groups') . ',' . + $dbh->table_name('carddav_group_user') . + ' WHERE id=group_id AND contact_id=?', $id); + + $res = array(); + while ($row = $dbh->fetch_assoc($sql_result)) { + $res[$row['id']] = $row['name']; + } + + return $res; + }}} + + /** + * Setter for the current group + */ + public function set_group($gid) + {{{ + $this->group_id = $gid; + $this->total_cards = -1; + if ($gid) { + $dbh = rcmail::get_instance()->db; + $this->filter = "EXISTS(SELECT * FROM ".$dbh->table_name("carddav_group_user")." + WHERE group_id = '{$gid}' AND contact_id = ".$dbh->table_name("carddav_contacts").".id)"; + } else { + $this->filter = ''; + } + }}} + + /** + * Get group properties such as name and email address(es) + * + * @param string Group identifier + * @return array Group properties as hash array + */ + function get_group($group_id) + { + $dbh = rcmail::get_instance()->db; + + $sql_result = $dbh->query('SELECT * FROM '. + $dbh->table_name('carddav_groups'). + ' WHERE id = ?', $group_id); + + if ($sql_result && ($sql_arr = $dbh->fetch_assoc($sql_result))) { + return $sql_arr; + } + + return null; + } + + /** + * List all active contact groups of this source + * + * @param string Optional search string to match group name + * @param int Search mode. Sum of self::SEARCH_* (>= 1.2.3) + * 0 - partial (*abc*), + * 1 - strict (=), + * 2 - prefix (abc*) + * + * @return array Indexed list of contact groups, each a hash array + */ + public function list_groups($search = null, $mode = 0) + {{{ + $dbh = rcmail::get_instance()->db; + + $searchextra = ""; + if ($search !== null){ + if ($mode & 1) { + $searchextra = $dbh->ilike('name', $search); + } elseif ($mode & 2) { + $searchextra = $dbh->ilike('name',"$search%"); + } else { + $searchextra = $dbh->ilike('name',"%$search%"); + } + $searchextra = ' AND ' . $searchextra; + } + + $sql_result = $dbh->query('SELECT id,name from ' . + $dbh->table_name('carddav_groups') . + ' WHERE abook_id=?' . + $searchextra . + ' ORDER BY name ASC', + $this->id); + + $groups = array(); + + while ($row = $dbh->fetch_assoc($sql_result)) { + $row['ID'] = $row['id']; + $groups[] = $row; + } + + return $groups; + }}} + + /** + * Create a contact group with the given name + * + * @param string The group name + * @return mixed False on error, array with record props in success + */ + public function create_group($name) + {{{ + $cuid = $this->find_free_uid(); + $uri = "$cuid.vcf"; + + $save_data = array( + 'name' => $name, + 'kind' => 'group', + 'cuid' => $cuid, + ); + + $vcf = $this->create_vcard_from_save_data($save_data); + if (!$vcf) return false; + $vcfstr = $vcf->serialize(); + if(!$this->config['use_categories']) { + if (!($etag = $this->put_record_to_carddav($uri, $vcfstr))) + return false; + + $url = carddav_common::concaturl($this->config['url'], $uri); + $url = preg_replace(';https?://[^/]+;', '', $url); + } else { + $etag="dummy".$name; + $url="dummy".$name; + } + if(!($dbid = $this->dbstore_group($etag,$url,$vcfstr,$save_data))) + return false; + + return array('id'=>$dbid, 'name'=>$name); + }}} + + /** + * Delete the given group and all linked group members + * + * @param string Group identifier + * @return boolean True on success, false if no data was changed + */ + public function delete_group($group_id) + {{{ + $ids = null; + // get current DB data + $group = self::get_dbrecord($group_id,'uri','groups'); + if(!$group) return false; + + if($this->config['use_categories']) { + $contacts = self::get_dbrecord($group_id, 'contact_id as id', 'group_user', false, 'group_id'); + $ids = array(); + foreach($contacts as $contact) { + $ids[]=$contact['id']; + } + } + if(!$this->config['use_categories']) { + if($this->delete_record_from_carddav($group['uri'])) { + self::delete_dbrecord($group_id, 'groups'); + self::delete_dbrecord($group_id, 'group_user', 'group_id'); + return true; + } + } else { + self::delete_dbrecord($group_id, 'groups'); + self::delete_dbrecord($group_id, 'group_user', 'group_id'); + } + if($this->config['use_categories']) { + $this->update_contacts($ids); + return true; + } + return false; + }}} + + /** + * Rename a specific contact group + * + * @param string Group identifier + * @param string New name to set for this group + * @param string New group identifier (if changed, otherwise don't set) + * @return boolean New name on success, false if no data was changed + */ + public function rename_group($group_id, $newname, &$newid) + {{{ + // get current DB data + $group = self::get_dbrecord($group_id,'uri,etag,vcard,name,cuid','groups'); + if(!$group) return false; + $group['name'] = $newname; + // create vcard from current DB data to be updated with the new data + + if(!$this->config['use_categories']) { + // create vcard from current DB data to be updated with the new data + try { + $vcf = VObject\Reader::read($group['vcard'], VObject\Reader::OPTION_FORGIVING); + } catch (Exception $e) { + self::$helper->warn("Update: Couldn't parse local group vcard: ".$group['vcard']); + return false; + } + + $vcf->FN = $newname; + $vcf->N = $newname; + $vcfstr = $vcf->serialize(); + + if(!($etag = $this->put_record_to_carddav($group['uri'], $vcfstr, $group['etag']))) + return false; + } + if(!$this->dbstore_group($etag,$group['uri'],$vcfstr,$group,$group_id)) + return false; + + if($this->config['use_categories']) { + $contacts = self::get_dbrecord($group_id, 'contact_id as id', 'group_user', false, 'group_id'); + $ids = array(); + foreach($contacts as $contact) { + $ids[]=$contact['id']; + } + $this->update_contacts($ids); + } + + return $newname; + }}} + + + /** + * Returns an array of categories for this card or a one-element array with + * the value 'Unfiled' if no CATEGORIES property is found. + */ + function getCategories(&$vcard) + { + $property = $vcard->{'CATEGORIES'}; + // The Mac OS X Address Book application uses the CATEGORY property + // instead of the CATEGORIES property. + if (!$property) { + $property = $vcard->{'CATEGORY'}; + } + if ($property) { + return $property->getParts(); + } + return array(); + } + + /** + * Returns true if the card belongs to at least one of the categories. + */ + function inCategories(&$vcard, &$categories) + { + $our_categories = $vcard->getCategories(); + foreach ($categories as $category) { + if (in_array_case($category, $our_categories)) { + return true; + } + } + return false; + } + + public static function get_dbrecord($id, $cols='*', $table='contacts', $retsingle=true, $idfield='id', $other_conditions = array()) + {{{ + $dbh = rcmail::get_instance()->db; + + $idfield = $dbh->quoteIdentifier($idfield); + $id = $dbh->quote($id); + $sql = "SELECT $cols FROM " . $dbh->table_name("carddav_$table") . ' WHERE ' . $idfield . '=' . $id; + + // Append additional conditions + foreach ($other_conditions as $field => $value) { + $sql .= ' AND ' . $dbh->quoteIdentifier($field) . ' = ' . $dbh->quote($value); + } + + $sql_result = $dbh->query($sql); + + // single row requested? + if($retsingle) + return $dbh->fetch_assoc($sql_result); + + // multiple rows requested + $ret = array(); + while($row = $dbh->fetch_assoc($sql_result)) + $ret[] = $row; + return $ret; + }}} + + public static function delete_dbrecord($ids, $table='contacts', $idfield='id', $other_conditions = array()) + {{{ + $dbh = rcmail::get_instance()->db; + + if(is_array($ids)) { + if(count($ids) <= 0) return 0; + foreach($ids as &$id) + $id = $dbh->quote(is_array($id)?$id[$idfield]:$id); + $dspec = ' IN ('. implode(',',$ids) .')'; + } else { + $dspec = ' = ' . $dbh->quote($ids); + } + + $idfield = $dbh->quoteIdentifier($idfield); + $sql = "DELETE FROM " . $dbh->table_name("carddav_$table") . " WHERE $idfield $dspec"; + + // Append additional conditions + foreach ($other_conditions as $field => $value) { + $sql .= ' AND ' . $dbh->quoteIdentifier($field) . ' = ' . $dbh->quote($value); + } + + $sql_result = $dbh->query($sql); + return $dbh->affected_rows($sql_result); + }}} + + public static function carddavconfig($abookid) + {{{ + $dbh = rcmail::get_instance()->db; + + // cludge, agreed, but the MDB abstraction seems to have no way of + // doing time calculations... + $timequery = '('. $dbh->now() . ' > '; + if ($dbh->db_provider === 'sqlite') { + $timequery .= ' datetime(last_updated,refresh_time))'; + } elseif ($dbh->db_provider === 'mysql') { + $timequery .= ' date_add(last_updated, INTERVAL refresh_time HOUR_SECOND))'; + } else { + $timequery .= ' last_updated+refresh_time)'; + } + + $abookrow = self::get_dbrecord($abookid, + 'id as abookid,name,username,use_categories,password,url,presetname,sync_token,authentication_scheme,' + . $timequery . ' as needs_update', 'addressbooks'); + + if(! $abookrow) { + self::$helper->warn("FATAL! Request for non-existent configuration $abookid"); + return false; + } + + if ($dbh->db_provider === 'postgres') { + // postgres will return 't'/'f' here for true/false, normalize it to 1/0 + $nu = $abookrow['needs_update']; + $nu = ($nu==1 || $nu=='t')?1:0; + $abookrow['needs_update'] = $nu; + } + + return $abookrow; + }}} + + public static function update_addressbook($dbid=0, $xcol=array(), $xval=array()) + {{{ + $dbh = rcmail::get_instance()->db; + + self::$helper->debug("UPDATE addressbook $dbid"); + $xval[]=$dbid; + $sql_result = $dbh->query('UPDATE ' . + $dbh->table_name("carddav_addressbooks") . + ' SET ' . implode('=?,', $xcol) . '=?' . + ' WHERE id=?', $xval); + + if($dbh->is_error()) { + self::$helper->warn($dbh->is_error()); + $this->set_error(self::ERROR_SAVING, $dbh->is_error()); + return false; + } + + return $dbid; + }}} + /** + * Migrates settings to a separate addressbook table. + */ + public static function migrateconfig($sub = 'CardDAV') + {{{ + $rcmail = rcmail::get_instance(); + $prefs_all = $rcmail->config->get('carddav', 0); + $dbh = $rcmail->db; + + // adopt password storing scheme if stored password differs from configured scheme + $sql_result = $dbh->query('SELECT id,password FROM ' . + $dbh->table_name('carddav_addressbooks') . + ' WHERE user_id=?', $_SESSION['user_id']); + + while ($abookrow = $dbh->fetch_assoc($sql_result)) { + $pw_scheme = self::$helper->password_scheme($abookrow['password']); + if(strcasecmp($pw_scheme, carddav_common::$pwstore_scheme) !== 0) { + $abookrow['password'] = self::$helper->decrypt_password($abookrow['password']); + $abookrow['password'] = self::$helper->encrypt_password($abookrow['password']); + $dbh->query('UPDATE ' . + $dbh->table_name('carddav_addressbooks') . + ' SET password=? WHERE id=?', + $abookrow['password'], + $abookrow['id']); + } + } + + // any old (Pre-DB) settings to migrate? + if(!$prefs_all) { + return; + } + + // migrate to the multiple addressbook schema first if needed + if ($prefs_all['db_version'] == 1 || !array_key_exists('db_version', $prefs_all)){ + self::$helper->debug("migrating DB1 to DB2"); + unset($prefs_all['db_version']); + $p = array(); + $p['CardDAV'] = $prefs_all; + $p['db_version'] = 2; + $prefs_all = $p; + } + + // migrate settings to database + foreach ($prefs_all as $desc => $prefs){ + // skip non address book attributes + if (!is_array($prefs)){ + continue; + } + + $crypt_password = self::$helper->encrypt_password($prefs['password']); + + self::$helper->debug("move addressbook $desc"); + $dbh->query('INSERT INTO ' . + $dbh->table_name('carddav_addressbooks') . + '(name,username,password,url,active,user_id) ' . + 'VALUES (?, ?, ?, ?, ?, ?)', + $desc, $prefs['username'], $crypt_password, $prefs['url'], + $prefs['use_carddav'], $_SESSION['user_id']); + } + + // delete old settings + $usettings = $rcmail->user->get_prefs(); + $usettings['carddav'] = array(); + self::$helper->debug("delete old prefs: " . $rcmail->user->save_prefs($usettings)); + }}} + + public function delete_all($with_groups = false) + {{{ + $dbh = rcmail::get_instance()->db; + $abook_id = $this->id; + $res1 = $dbh->query('SELECT id FROM '. + $dbh->table_name('carddav_contacts'). + ' WHERE abook_id=?',$abook_id); + $contact_ids = array(); + while($row = $dbh->fetch_assoc($res1)) { + $contact_ids[] = $row['id']; + } + $this->delete($contact_ids); + + if ($with_groups != false) { + $res2 = $dbh->query('SELECT id FROM '. + $dbh->table_name('carddav_groups'). + ' WHERE abook_id=?',$abook_id); + while($row = $dbh->fetch_assoc($res2)) { + $this->delete_group($row['id']); + } + } + }}} + + public static function initClass() + {{{ + self::$helper = new carddav_common('BACKEND: '); + }}} +} + +carddav_backend::initClass(); + +?> diff --git a/plugins/carddav/carddav_common.php b/plugins/carddav/carddav_common.php new file mode 100644 index 0000000..21d1d23 --- /dev/null +++ b/plugins/carddav/carddav_common.php @@ -0,0 +1,397 @@ +, + Michael Stilkerich + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +if (file_exists(__DIR__ . '/vendor/autoload.php')) + require_once __DIR__ . '/vendor/autoload.php'; + +\Httpful\Bootstrap::init(); + +class carddav_common +{ + const DEBUG = false; // set to true for basic debugging + const DEBUG_HTTP = false; // set to true for debugging raw http stream + + const NSDAV = 'DAV:'; + const NSCARDDAV = 'urn:ietf:params:xml:ns:carddav'; + + // admin settings from config.inc.php + private static $admin_settings; + // encryption scheme + public static $pwstore_scheme = 'base64'; + + private $module_prefix = ''; + + public function __construct($module_prefix = '') + {{{ + $this->module_prefix = $module_prefix; + }}} + + public static function concaturl($str, $cat) + {{{ + preg_match(";(^https?://[^/]+)(.*);", $str, $match); + $hostpart = $match[1]; + $urlpart = $match[2]; + + // is $cat already a full URL? + if(strpos($cat, '://') !== FALSE) { + return $cat; + } + + // is $cat a simple filename? + // then attach it to the URL + if (substr($cat, 0, 1) != "/"){ + $urlpart .= "/$cat"; + + // $cat is a full path, the append it to the + // hostpart only + } else { + $urlpart = $cat; + } + + // remove // in the path + $urlpart = preg_replace(';//+;','/',$urlpart); + return $hostpart.$urlpart; + }}} + + // log helpers + private function getCaller() + {{{ + // determine calling function for debug output + if (version_compare(PHP_VERSION, "5.4", ">=")){ + $caller=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS,3); + } else { + $caller=debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + } + $caller=$caller[2]['function']; + return $caller; + }}} + + public function warn() + {{{ + $caller=self::getCaller(); + rcmail::write_log("carddav.warn", $this->module_prefix . "($caller) " . implode(' ', func_get_args())); + }}} + + public function debug() + {{{ + if(self::DEBUG) { + $caller=self::getCaller(); + rcmail::write_log("carddav", $this->module_prefix . "($caller) " . implode(' ', func_get_args())); + } + }}} + + public function debug_http() + {{{ + if(self::DEBUG_HTTP) { + $caller=self::getCaller(); + rcmail::write_log("carddav", $this->module_prefix . "($caller) " . implode(' ', func_get_args())); + } + }}} + + // XML helpers + public function checkAndParseXML($reply) { + if(!is_array($reply)) + return false; + + if(!self::check_contenttype($reply['headers']['content-type'], ';(text|application)/xml;')) + return false; + + $xml = new SimpleXMLElement($reply['body']); + $this->registerNamespaces($xml); + return $xml; + } + + public function registerNamespaces($xml) { + // Use slightly complex prefixes to avoid conflicts + $xml->registerXPathNamespace('RCMCC', self::NSCARDDAV); + $xml->registerXPathNamespace('RCMCD', self::NSDAV); + } + + // HTTP helpers + /** + * @param $url: url of the requested resource + * + * @param $http_opts: Options for the HTTP request, keys: + * - method: request method (GET, PROPFIND, etc.) + * - content: request body + * - header: array of HTTP request headers as simple strings + * + * @param $carddav: config array containing at least the keys + * - url: base url, used if $url is a relative url + * - username + * - password: password (encoded/encrypted form as stored in DB) + */ + public function cdfopen($url, $http_opts, $carddav) + {{{ + $redirect_limit = 5; + $rcmail = rcmail::get_instance(); + + $username=$carddav['username']; + $password = self::decrypt_password($carddav['password']); + $baseurl=$carddav['url']; + + // determine calling function for debug output + $caller=self::getCaller(); + + $local = $rcmail->user->get_username('local'); + $domain = $rcmail->user->get_username('domain'); + + // Substitute Placeholders + $username = str_replace( '%u', $_SESSION['username'], $username); + $username = str_replace( '%V' ,str_replace('@','_', str_replace('.','_',$_SESSION['username'])), $username); + $username = str_replace( '%l', $local, $username); + $username = str_replace( '%d', $domain, $username); + if($password == '%p') + $password = $rcmail->decrypt($_SESSION['password']); + $baseurl = str_replace("%u", $username, $carddav['url']); + $url = str_replace("%u", $username, $url); + $baseurl = str_replace("%l", $local, $baseurl); + $url = str_replace("%l", $local, $url); + $baseurl = str_replace("%d", $domain, $baseurl); + $url = str_replace("%d", $domain, $url); + + // if $url is relative, prepend the base url + $url = self::concaturl($baseurl, $url); + + do { + $isRedirect = false; + if (self::DEBUG){ $this->debug("$caller requesting $url as user $username [RL $redirect_limit]"); } + + $httpful = \Httpful\Request::init(); + $scheme = strtolower($carddav['authentication_scheme']); + if ($scheme != "basic" && $scheme != "digest" && $scheme != "negotiate"){ + /* figure out authentication */ + $httpful->addHeader("User-Agent", "RCM CardDAV plugin/3.0.3"); + $httpful->uri($url); + $httpful->method($http_opts['method']); + $error = $httpful->send(); + + $httpful = \Httpful\Request::init(); + $scheme = "unknown"; + // Using raw_headers since there might be multiple www-authenticate headers + if (preg_match("/^(.*\n)*WWW-Authenticate:\s+Negotiate\b/i", $error->raw_headers) && !empty($_SERVER['KRB5CCNAME'])){ + $httpful->negotiateAuth($username, $password); + $scheme = "negotiate"; + } else if (preg_match("/\bDigest\b/i", $error->headers["www-authenticate"])){ + $httpful->digestAuth($username, $password); + $scheme = "digest"; + } else if (preg_match("/\bBasic\b/i", $error->headers["www-authenticate"])){ + $httpful->basicAuth($username, $password); + $scheme = "basic"; + } + + if ($scheme != "unknown") + carddav_backend::update_addressbook($carddav['abookid'], array("authentication_scheme"), array($scheme)); + } else { + // if we have KRB5CCNAME, use negotiate even if current scheme is basic + if ((strtolower($scheme) == "negotiate" || strtolower($scheme) == "basic") && !empty($_SERVER['KRB5CCNAME'])) { + $httpful->negotiateAuth($username, $password); + } else if (strtolower($scheme) == "digest"){ + $httpful->digestAuth($username, $password); + // if we don't have KRB5CCNAME, use basic even if current scheme is negotiate + } else if (strtolower($scheme) == "negotiate" || strtolower($scheme) == "basic"){ + $httpful->basicAuth($username, $password); + } + } + + $httpful->addHeader("User-Agent", "RCM CardDAV plugin/3.0.3"); + $httpful->uri($url); + + $httpful->method($http_opts['method']); + if (array_key_exists('content',$http_opts) && strlen($http_opts['content'])>0 && $http_opts['method'] != "GET"){ + $httpful->body($http_opts['content']); + } + + if(array_key_exists('header',$http_opts)) { + foreach ($http_opts['header'] as $header){ + $h = explode(": ", $header); + if (strlen($h[0]) > 0 && strlen($h[1]) > 0){ + // Only append headers with key AND value + $httpful->addHeader($h[0], $h[1]); + } + } + } + + $reply = $httpful->send(); + $scode = $reply->code; + if (self::DEBUG){ $this->debug("Code: $scode"); } + + $isRedirect = ($scode>300 && $scode<304) || $scode==307; + if($isRedirect && strlen($reply->headers['location'])>0) { + $url = self::concaturl($baseurl, $reply->headers['location']); + } else { + $retVal["status"] = $scode; + $retVal["headers"] = $reply->headers; + $retVal["body"] = $reply->raw_body; + if (self::DEBUG_HTTP){ $this->debug_http("success: ".var_export($retVal, true)); } + return $retVal; + } + } while($redirect_limit-->0 && $isRedirect); + + return $reply->code; + }}} + + public function check_contenttype($ctheader, $expectedct) + {{{ + if(!is_array($ctheader)) { + $ctheader = array($ctheader); + } + + foreach($ctheader as $ct) { + if(preg_match($expectedct, $ct)) + return true; + } + + return false; + }}} + + // password helpers + private function carddav_des_key() + {{{ + $rcmail = rcmail::get_instance(); + $imap_password = $rcmail->decrypt($_SESSION['password']); + while(strlen($imap_password)<24) { + $imap_password .= $imap_password; + } + return substr($imap_password, 0, 24); + }}} + + public function encrypt_password($clear) + {{{ + if(strcasecmp(self::$pwstore_scheme, 'plain')===0) + return $clear; + + if(strcasecmp(self::$pwstore_scheme, 'encrypted')===0) { + + // return {IGNORE} scheme if session password is empty (krb_authentication plugin) + if(empty($_SESSION['password'])) return '{IGNORE}'; + + // encrypted with IMAP password + $rcmail = rcmail::get_instance(); + + $imap_password = self::carddav_des_key(); + $deskey_backup = $rcmail->config->set('carddav_des_key', $imap_password); + + $crypted = $rcmail->encrypt($clear, 'carddav_des_key'); + + // there seems to be no way to unset a preference + $deskey_backup = $rcmail->config->set('carddav_des_key', ''); + + return '{ENCRYPTED}'.$crypted; + } + + if(strcasecmp(self::$pwstore_scheme, 'des_key')===0) { + + // encrypted with global des_key + $rcmail = rcmail::get_instance(); + $crypted = $rcmail->encrypt($clear); + + return '{DES_KEY}'.$crypted; + } + + // default: base64-coded password + return '{BASE64}'.base64_encode($clear); + }}} + + public function password_scheme($crypt) + {{{ + if(strpos($crypt, '{IGNORE}') === 0) + return 'ignore'; + + if(strpos($crypt, '{ENCRYPTED}') === 0) + return 'encrypted'; + + if(strpos($crypt, '{DES_KEY}') === 0) + return 'des_key'; + + if(strpos($crypt, '{BASE64}') === 0) + return 'base64'; + + // unknown scheme, assume cleartext + return 'plain'; + }}} + + public function decrypt_password($crypt) + {{{ + if(strpos($crypt, '{ENCRYPTED}') === 0) { + // return {IGNORE} scheme if session password is empty (krb_authentication plugin) + if (empty($_SESSION['password'])) return '{IGNORE}'; + + $crypt = substr($crypt, strlen('{ENCRYPTED}')); + $rcmail = rcmail::get_instance(); + + $imap_password = self::carddav_des_key(); + $deskey_backup = $rcmail->config->set('carddav_des_key', $imap_password); + + $clear = $rcmail->decrypt($crypt, 'carddav_des_key'); + + // there seems to be no way to unset a preference + $deskey_backup = $rcmail->config->set('carddav_des_key', ''); + + return $clear; + } + + if(strpos($crypt, '{DES_KEY}') === 0) { + $crypt = substr($crypt, strlen('{DES_KEY}')); + $rcmail = rcmail::get_instance(); + + return $rcmail->decrypt($crypt); + } + + if(strpos($crypt, '{BASE64}') === 0) { + $crypt = substr($crypt, strlen('{BASE64}')); + return base64_decode($crypt); + } + + // unknown scheme, assume cleartext + return $crypt; + }}} + + // admin settings from config.inc.php + public static function get_adminsettings() + {{{ + if(is_array(self::$admin_settings)) + return self::$admin_settings; + + $rcmail = rcmail::get_instance(); + $prefs = array(); + $configfile = dirname(__FILE__)."/config.inc.php"; + if (file_exists($configfile)){ + require("$configfile"); + } + self::$admin_settings = $prefs; + + if(is_array($prefs['_GLOBAL'])) { + $scheme = $prefs['_GLOBAL']['pwstore_scheme']; + if(preg_match("/^(plain|base64|encrypted|des_key)$/", $scheme)) + self::$pwstore_scheme = $scheme; + } + return $prefs; + }}} + + // short form for deprecated Q helper function + public function Q($str, $mode='strict', $newlines=true) + {{{ + return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines); + }}} +} + +?> diff --git a/plugins/carddav/carddav_discovery.php b/plugins/carddav/carddav_discovery.php new file mode 100644 index 0000000..8b3e097 --- /dev/null +++ b/plugins/carddav/carddav_discovery.php @@ -0,0 +1,280 @@ +, + Michael Stilkerich + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + + +require_once("carddav_backend.php"); +require_once("carddav_common.php"); + +class carddav_discovery +{ + private static $helper; + + /** + * Determines the location of the addressbook for the current user on the + * CardDAV server. + * + * Returns: Array of found addressbook. Each array element is array with keys: + * - name: Name of the addressbook reported by server + * - href: URL to the addressbook collection + * + * On error, false is returned. + */ + public function find_addressbooks($url, $user, $password) + {{{ + if (!preg_match(';^(([^:]+)://)?(([^/:]+)(:([0-9]+))?)(/?.*)$;', $url, $match)) + return false; + + $protocol = $match[2]; // optional + $host = $match[4]; // mandatory + $port = $match[6]; // optional + $path = $match[7]; // optional + + // plain is only used if http was explicitly given + $use_ssl = !($protocol == "http"); + + // setup default values if no user values given + if($use_ssl) { + $protocol = $protocol?$protocol:'https'; + $port = $port ?$port :443; + } else { + $protocol = $protocol?$protocol:'http'; + $port = $port ?$port :80; + } + + $services = $this->find_servers($host, $use_ssl); + + // Fallback: If no DNS provided, we use the data given by the user/defaults + if(count($services) == 0) { + $services[] = array( + 'host' => $host, + 'port' => ($port ? $port : ($use_ssl ? 443 : 80)), + 'baseurl' => "$protocol://$host:$port", + ); + } + + $services = $this->find_baseurls($services); + + // if the user specified a full URL, we try that first + if(strlen($path) > 2) { + $userspecified = array( + 'host' => $host, + 'port' => ($port ? $port : ($use_ssl ? 443 : 80)), + 'baseurl' => "$protocol://$host:$port", + 'paths' => array($path), + ); + array_unshift($services, $userspecified); + } + + $cdfopen_cfg = array('username'=>$user, 'password'=>$password); + + // now check each of them until we find something (or don't) + foreach($services as $service) { + $cdfopen_cfg['url'] = $service['baseurl']; + + foreach($service['paths'] as $path) { + $aBooks = $this->retrieve_addressbooks($path, $cdfopen_cfg); + if(is_array($aBooks) && count($aBooks)>0) + return $aBooks; + } + } + return false; + }}} + + private function retrieve_addressbooks($path, $cdfopen_cfg, $recurse=false) + {{{ + $baseurl = $cdfopen_cfg['url']; + $url = carddav_common::concaturl($baseurl, $path); + $depth = ($recurse ? 1 : 0); + self::$helper->debug("SEARCHING $url (Depth: ".$depth.")"); + + // check if the given URL points to an addressbook + $opts = array( + 'method'=>"PROPFIND", + 'header'=>array("Depth: " . $depth, 'Content-Type: application/xml; charset="utf-8"'), + 'content'=> << + + + + + + +EOF + ); + + $reply = self::$helper->cdfopen($url, $opts, $cdfopen_cfg); + $xml = self::$helper->checkAndParseXML($reply); + if($xml === false) return false; + + $aBooks = array(); + + // Check if we found addressbooks at the URL + $xpresult = $xml->xpath('//RCMCD:response[descendant::RCMCD:resourcetype/RCMCC:addressbook]'); + foreach($xpresult as $ab) { + self::$helper->registerNamespaces($ab); + $aBook = array(); + list($aBook['href']) = $ab->xpath('child::RCMCD:href'); + list($aBook['name']) = $ab->xpath('descendant::RCMCD:displayname'); + $aBook['href'] = (string) $aBook['href']; + $aBook['name'] = (string) $aBook['name']; + + if(strlen($aBook['href']) > 0) { + $aBook['href'] = carddav_common::concaturl($baseurl, $aBook['href']); + self::$helper->debug("found abook: ".$aBook['name']." at ".$aBook['href']); + $aBooks[] = $aBook; + } + } + + // found -> done + if(count($aBooks) > 0) return $aBooks; + + if ($recurse === false) { + // Check some additional URLs: + $additional_urls = array(); + + // (1) original URL + $additional_urls[] = $url; + + // (2) see if the server told us the addressbook home location + $abookhome = $xml->xpath('//RCMCC:addressbook-home-set/RCMCD:href'); + if (count($abookhome) != 0) { + self::$helper->debug("addressbook home: ".$abookhome[0]); + $additional_urls[] = $abookhome[0]; + } + // (3) see if we got a principal URL + $princurl = $xml->xpath('//RCMCD:current-user-principal/RCMCD:href'); + if (count($princurl) != 0) { + self::$helper->debug("principal URL: ".$princurl[0]); + $additional_urls[] = $princurl[0]; + } + + foreach($additional_urls as $other_url) { + self::$helper->debug("Searching additional URL: $other_url"); + if(strlen($other_url) <= 0) continue; + + // if the server returned a full URL, adjust the base url + if (preg_match(';^[^/]+://[^/]+;', $other_url, $match)) { + $cdfopen_cfg['url'] = $match[0]; + } else { + // restore original baseurl, may have changed in prev URL check + $cdfopen_cfg['url'] = $baseurl; + } + $aBooks = $this->retrieve_addressbooks($other_url, $cdfopen_cfg, $other_url != $princurl[0]); + // found -> done + if (!($aBooks === false) && count($aBooks) > 0) return $aBooks; + } + } + + // (4) there is no more we can do -> fail + self::$helper->debug("no principal URL found"); + return false; + }}} + + // get services by querying DNS SRV records + private function find_servers($host, $ssl) + {{{ + if($ssl) { + $srvpfx = '_carddavs'; + $defport = 443; + $protocol = 'https'; + } else { + $srvpfx = '_carddav'; + $defport = 80; + $protocol = 'http'; + } + + $srv = "$srvpfx._tcp.$host"; + + // query SRV records + $dnsresults = dns_get_record($srv, DNS_SRV); + + // order according to priority and weight + // TODO weight is not quite correctly handled atm, see RFC2782, + // but this is not crucial to functionality + $sortPrioWeight = function($a, $b) { + if ($a['pri'] != $b['pri']) { + return $b['pri'] - $a['pri']; + } + + return $a['weight'] - $b['weight']; + }; + + usort($dnsresults, $sortPrioWeight); + + // build results + $result = array(); + foreach($dnsresults as $dnsres) { + $target = $dnsres['target']; + $port = $dnsres['port'] ? $dnsres['port'] : $defport; + $baseurl = "$protocol://$target:$port"; + if($target) { + self::$helper->debug("found service: $baseurl"); + + $result[] = array( + 'host' => $target, + 'port' => $port, + 'baseurl' => $baseurl, + 'dnssrv' => "$srvpfx._tcp.$target", + ); + } + } + + return $result; + }}} + + // discover path and add default paths to services + private function find_baseurls($services) + {{{ + foreach($services as &$service) { + $baseurl = $service['baseurl']; + $dnssrv = $service['dnssrv']; + + $paths = array(); + + $dnsresults = dns_get_record($dnssrv, DNS_TXT); + foreach($dnsresults as $dnsresult) { + if($dnsresult['host'] != $dnssrv) continue; + + foreach($dnsresult['entries'] as $ent) { + if (preg_match('^path=(.+)', $ent, $match)) + $paths[] = $match[1]; + } + } + + // as fallback try these default paths + $paths[] = '/.well-known/carddav'; + $paths[] = '/'; + + $service['paths'] = $paths; + } + + return $services; + }}} + + public static function initClass() + {{{ + self::$helper = new carddav_common('DISCOVERY: '); + }}} +} + +carddav_discovery::initClass(); + +?> diff --git a/plugins/carddav/composer.json b/plugins/carddav/composer.json new file mode 100644 index 0000000..6d72772 --- /dev/null +++ b/plugins/carddav/composer.json @@ -0,0 +1,38 @@ +{ + "name": "roundcube/carddav", + "type": "roundcube-plugin", + "description": "CardDAV adapter for connecting to CardDAV-enabled addressbooks", + "keywords": ["addressbook","carddav","contacts","owncloud","davical"], + "homepage": "https://www.benjamin-schieder.de/carddav.html", + "license": "GPL-2.0", + "version": "3.0.3", + "authors": [ + { + "name": "Benjamin Schieder", + "email": "carddav@wegwerf.anderdonau.de", + "homepage": "https://www.benjamin.schieder.de/", + "role": "Developer" + }, + { + "name": "Michael Stilkerich", + "email": "michael@stilkerich.eu", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "vcs", + "url": "git://github.com/blind-coder/rcmcarddav.git" + } + ], + "require": { + "php": ">=5.6.18", + "nategood/httpful": "~0.2", + "sabre/vobject": "~3.4" + }, + "extra": { + "roundcube": { + "min-version": "1.0.0" + } + } +} diff --git a/plugins/carddav/composer.lock b/plugins/carddav/composer.lock new file mode 100644 index 0000000..74ef569 --- /dev/null +++ b/plugins/carddav/composer.lock @@ -0,0 +1,136 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "c645717601239af47e93d45ac34cb26e", + "packages": [ + { + "name": "nategood/httpful", + "version": "0.2.20", + "source": { + "type": "git", + "url": "https://github.com/nategood/httpful.git", + "reference": "c1cd4d46a4b281229032cf39d4dd852f9887c0f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nategood/httpful/zipball/c1cd4d46a4b281229032cf39d4dd852f9887c0f6", + "reference": "c1cd4d46a4b281229032cf39d4dd852f9887c0f6", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "Httpful": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nate Good", + "email": "me@nategood.com", + "homepage": "http://nategood.com" + } + ], + "description": "A Readable, Chainable, REST friendly, PHP HTTP Client", + "homepage": "http://github.com/nategood/httpful", + "keywords": [ + "api", + "curl", + "http", + "requests", + "rest", + "restful" + ], + "time": "2015-10-26T16:11:30+00:00" + }, + { + "name": "sabre/vobject", + "version": "3.5.3", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "129d80533a9ec0d9cacfb50b51180c34edb6874c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/129d80533a9ec0d9cacfb50b51180c34edb6874c", + "reference": "129d80533a9ec0d9cacfb50b51180c34edb6874c", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.1" + }, + "require-dev": { + "phpunit/phpunit": "*", + "squizlabs/php_codesniffer": "*" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "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", + "jCal", + "jCard", + "vCard" + ], + "time": "2016-10-07T03:20:40+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.18" + }, + "platform-dev": [] +} diff --git a/plugins/carddav/config.inc.php.dist b/plugins/carddav/config.inc.php.dist new file mode 100644 index 0000000..5a64a6b --- /dev/null +++ b/plugins/carddav/config.inc.php.dist @@ -0,0 +1,217 @@ +'] = array( + // required attributes + 'name' => '', + 'username' => '', + 'password' => '', + 'url' => '', + + // optional attributes + 'active' => , + 'readonly' => , + 'refresh_time' => '', + + // attributes that are fixed (i.e., not editable by the user) and + // auto-updated for this preset + 'fixed' => array( < 0 or more of the other attribute keys > ), + + // always require these attributes, even for addressbook view + 'require_always' => ['email'], + + // hide this preset from CalDAV preferences section so users can't even + // see it + 'hide' => , +); +*/ + +// All values in angle brackets have to be substituted. +// +// The meaning of the different parameters is as follows: +// +// : Unique preset name, must not be '_GLOBAL'. The presetname is +// not user visible and only used for an internal mapping between +// addressbooks created from a preset and the preset itself. You +// should never change this throughout its lifetime. +// +// The following parameters are REQUIRED and need to be specified for any preset. +// +// name: User-visible name of the addressbook. If the server provides +// an additional display name for the addressbooks found for the +// preset, it will be appended in brackets to this name, except +// if carddav_name_only is true (see below). +// +// username: CardDAV username to access the addressbook. Set this setting +// to '%u' to use the roundcube username. +// In case one uses an email address as username there is the +// additional option to choose '%l', which will only use the +// local part of the username (eg: user.name@example.com will +// become user.name). +// Also, %d is available to get only the domain part of the +// username (eg: user.name@example.com will become example.com). +// In some cases %V might be usefull: it replaces @ and . by _. +// (eg: user.name@example.com will become user_name_example_com) +// +// password: CardDAV password to access the addressbook. Set this setting +// to '%p' to use the roundcube password. The password will not +// be stored in the database when using %p. +// +// url: URL where to find the CardDAV addressbook(s). If the given URL +// refers directly to an addressbook, only this single +// addressbook will be added. If the URL points somewhere in the +// CardDAV space, but _not_ to the location of a particular +// addressbook, the server will be queried for the available +// addressbooks and all of them will be added. You can use %u +// within the URL as a placeholder for the CardDAV username. +// '%l' works the same way as it does for the username field. +// +// The following parameters are OPTIONAL and need to be specified only if the default +// value is not acceptable. +// +// active: If this parameter is false, the addressbook is not used by roundcube +// unless the user changes this setting. +// Default: true +// +// carddav_name_only: +// If this parameter is true, only the server provided displayname +// is used for addressbooks created from this preset, except if +// the server does not provide a display name. +// Default: false +// +// readonly: If this parameter is true, the addressbook will only be +// accessible in read-only mode, i.e., the user will not be able +// to add, modify or delete contacts in the addressbook. +// Default: false +// +// refresh_time: Time interval for that cached versions of the addressbook +// entries should be used, in hours. After this time interval has +// passed since the last pull from the server, it will be +// refreshed when the addressbook is accessed the next time. +// Default: 01:00:00 +// +// use_categories: If this parameter is true, the addressbook will use the +// categories as groups unless the user changes this setting. +// Default: false +// +// fixed: Array of parameter keys that must not be changed by the user. +// Note that only fixed parameters will be automatically updated +// for existing addressbooks created from presets. Otherwise the +// user may already have changed the setting, and his change +// would be lost. You can add any of the above keys, but it the +// setting only affects parameters that can be changed via the +// settings pane (e.g., readonly cannot be changed by the user +// anyway). Still only parameters listed as fixed will +// automatically updated if the preset is changed. +// Default: empty, all settings modifiable by user +// +// !!! WARNING: Only add 'url' to the list of fixed addressbooks +// if it _directly_ points to an address book collection. +// Otherwise, the plugin will initially lookup the URLs for the +// collections on the server, and at the next login overwrite it +// with the fixed value stored here. Therefore, if you change the +// URL, you have two options: +// 1) If the new URL is a variation of the old one (e.g. hostname +// change), you can run an SQL UPDATE query directly in the +// database to adopt all addressbooks. +// 2) If the new URL is not easily derivable from the old one, +// change the key of the preset and change the URL. Addressbooks +// belonging to the old preset will be deleted upon the next +// login of the user and freshly created. +// +// require_always: If set, this database field is required to be non-empty +// for ALL queries, even just for displaying members. This may be +// useful if you have shared, read-only addressbooks with a lot +// of contacts that do not have an email address. +// +// hide: Whether this preset should be hidden from the CalDAV listing +// on the preferences page. + + +// How Preset Updates work +// +// Preset addressbooks are created for a user as she logs in. + +//// ** ADDRESSBOOK PRESETS - EXAMPLE: Two Addressbook Presets + +//// Preset 1: Personal +/* +$prefs['Personal'] = array( + // required attributes + 'name' => 'Personal', + // will be substituted for the roundcube username + 'username' => '%u', + // will be substituted for the roundcube password + 'password' => '%p', + // %u will be substituted for the CardDAV username + 'url' => 'https://ical.example.org/caldav.php/%u/Personal', + + 'active' => true, + 'readonly' => false, + 'refresh_time' => '02:00:00', + + 'fixed' => array( 'username' ), + 'hide' => false, +); +*/ + +//// Preset 2: Corporate +/* +$prefs['Work'] = array( + 'name' => 'Corporate', + 'username' => 'CorpUser', + 'password' => 'C0rpPasswo2d', + 'url' => 'https://ical.example.org/caldav.php/%u/Corporate', + + 'fixed' => array( 'name', 'username', 'password' ), + 'hide' => true, +); +*/ diff --git a/plugins/carddav/dbmigrations/0000-dbinit/mysql.sql b/plugins/carddav/dbmigrations/0000-dbinit/mysql.sql new file mode 100644 index 0000000..48959c9 --- /dev/null +++ b/plugins/carddav/dbmigrations/0000-dbinit/mysql.sql @@ -0,0 +1,84 @@ +-- table to store the configured address books +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_addressbooks ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(64) NOT NULL, + username VARCHAR(64) NOT NULL, + password VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + active TINYINT UNSIGNED NOT NULL DEFAULT 1, + user_id INT(10) UNSIGNED NOT NULL, + last_updated TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:01', -- time stamp of the last update of the local database + refresh_time TIME NOT NULL DEFAULT '01:00:00', -- time span after that the local database will be refreshed, default 1h + sync_token VARCHAR(255) NOT NULL DEFAULT '', -- sync-token the server sent us for the last sync + authentication_scheme VARCHAR(64) NOT NULL DEFAULT "auto", -- the HTTP authentication scheme to use, auto will be overwritten + + presetname VARCHAR(64), -- presetname + + PRIMARY KEY(id), + FOREIGN KEY (user_id) REFERENCES TABLE_PREFIXusers(user_id) ON DELETE CASCADE ON UPDATE CASCADE +) CHARACTER SET utf8 COLLATE utf8_unicode_ci /*!40000 ENGINE=INNODB */; + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_contacts ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + abook_id INT UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, -- display name + email VARCHAR(255), -- ", " separated list of mail addresses + firstname VARCHAR(255), + surname VARCHAR(255), + organization VARCHAR(255), + showas VARCHAR(32) NOT NULL DEFAULT '', -- special display type (e.g., as a company) + vcard LONGTEXT NOT NULL, -- complete vcard + etag VARCHAR(255) NOT NULL, -- entity tag, can be used to check if card changed on server + uri VARCHAR(255) NOT NULL, -- path of the card on the server + cuid VARCHAR(255) NOT NULL, -- unique identifier of the card within the collection + + PRIMARY KEY(id), + INDEX (abook_id), + UNIQUE INDEX(uri,abook_id), + UNIQUE INDEX(cuid,abook_id), + FOREIGN KEY (abook_id) REFERENCES TABLE_PREFIXcarddav_addressbooks(id) ON DELETE CASCADE ON UPDATE CASCADE +) CHARACTER SET utf8 COLLATE utf8_unicode_ci /*!40000 ENGINE=INNODB */; + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_xsubtypes ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + typename VARCHAR(128) NOT NULL, -- name of the type + subtype VARCHAR(128) NOT NULL, -- name of the subtype + abook_id INT UNSIGNED NOT NULL, + PRIMARY KEY(id), + UNIQUE INDEX(typename,subtype,abook_id), + FOREIGN KEY (abook_id) REFERENCES TABLE_PREFIXcarddav_addressbooks(id) ON DELETE CASCADE ON UPDATE CASCADE +) CHARACTER SET utf8 COLLATE utf8_unicode_ci /*!40000 ENGINE=INNODB */; + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_groups ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, + abook_id INT UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, -- display name + vcard TEXT NOT NULL, -- complete vcard + etag VARCHAR(255) NOT NULL, -- entity tag, can be used to check if card changed on server + uri VARCHAR(255) NOT NULL, -- path of the card on the server + cuid VARCHAR(255) NOT NULL, -- unique identifier of the card within the collection + + PRIMARY KEY(id), + UNIQUE(uri,abook_id), + UNIQUE(cuid,abook_id), + + FOREIGN KEY (abook_id) REFERENCES TABLE_PREFIXcarddav_addressbooks(id) ON DELETE CASCADE ON UPDATE CASCADE +) CHARACTER SET utf8 COLLATE utf8_unicode_ci /*!40000 ENGINE=INNODB */; + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_group_user ( + group_id INT UNSIGNED NOT NULL, + contact_id INT UNSIGNED NOT NULL, + + PRIMARY KEY(group_id,contact_id), + FOREIGN KEY(group_id) REFERENCES TABLE_PREFIXcarddav_groups(id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(contact_id) REFERENCES TABLE_PREFIXcarddav_contacts(id) ON DELETE CASCADE ON UPDATE CASCADE +) CHARACTER SET utf8 COLLATE utf8_unicode_ci /*!40000 ENGINE=INNODB */; + +CREATE TABLE TABLE_PREFIXcarddav_migrations ( + `ID` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY , + `filename` VARCHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL , + `processed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , + UNIQUE ( + `filename` + ) +) ENGINE = InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci; diff --git a/plugins/carddav/dbmigrations/0000-dbinit/postgres.sql b/plugins/carddav/dbmigrations/0000-dbinit/postgres.sql new file mode 100644 index 0000000..3dfbf9a --- /dev/null +++ b/plugins/carddav/dbmigrations/0000-dbinit/postgres.sql @@ -0,0 +1,105 @@ +CREATE SEQUENCE TABLE_PREFIXcarddav_addressbooks_seq + INCREMENT BY 1 + NO MAXVALUE + MINVALUE 1 + CACHE 1; + +-- table to store the configured address books + +CREATE TABLE TABLE_PREFIXcarddav_addressbooks ( + id integer DEFAULT nextval('TABLE_PREFIXcarddav_addressbooks_seq'::text) PRIMARY KEY, + name VARCHAR(64) NOT NULL, + username VARCHAR(64) NOT NULL, + password VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + active SMALLINT NOT NULL DEFAULT 1, + user_id integer NOT NULL REFERENCES TABLE_PREFIXusers (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + last_updated TIMESTAMP NOT NULL DEFAULT '-infinity', -- time stamp of the last update of the local database + refresh_time INTERVAL NOT NULL DEFAULT '01:00:00', -- time span after that the local database will be refreshed, default 1h + sync_token VARCHAR(255) NOT NULL DEFAULT '', -- sync-token the server sent us for the last sync + authentication_scheme VARCHAR(64) NOT NULL DEFAULT 'auto', -- the HTTP authentication scheme to use, auto will be overwritten + + presetname VARCHAR(64) -- presetname +); + +CREATE SEQUENCE TABLE_PREFIXcarddav_contacts_seq + INCREMENT BY 1 + NO MAXVALUE + MINVALUE 1 + CACHE 1; + +CREATE TABLE TABLE_PREFIXcarddav_contacts ( + id integer DEFAULT nextval('TABLE_PREFIXcarddav_contacts_seq'::text) PRIMARY KEY, + abook_id integer NOT NULL REFERENCES TABLE_PREFIXcarddav_addressbooks (id) ON DELETE CASCADE ON UPDATE CASCADE, + name VARCHAR(255) NOT NULL, -- display name + email VARCHAR(255), -- ", " separated list of mail addresses + firstname VARCHAR(255), + surname VARCHAR(255), + organization VARCHAR(255), + showas VARCHAR(32) NOT NULL DEFAULT '', -- special display type (e.g., as a company) + vcard text NOT NULL, -- complete vcard + etag VARCHAR(255) NOT NULL, -- entity tag, can be used to check if card changed on server + uri VARCHAR(255) NOT NULL, -- path of the card on the server + cuid VARCHAR(255) NOT NULL, -- unique identifier of the card within the collection + + UNIQUE(uri,abook_id), + UNIQUE(cuid,abook_id) +); + +CREATE INDEX TABLE_PREFIXcarddav_contacts_abook_id_idx ON TABLE_PREFIXcarddav_contacts(abook_id); + +CREATE SEQUENCE TABLE_PREFIXcarddav_xsubtypes_seq + INCREMENT BY 1 + NO MAXVALUE + MINVALUE 1 + CACHE 1; + +CREATE TABLE TABLE_PREFIXcarddav_xsubtypes ( + id integer DEFAULT nextval('TABLE_PREFIXcarddav_xsubtypes_seq'::text) PRIMARY KEY, + typename VARCHAR(128) NOT NULL, -- name of the type + subtype VARCHAR(128) NOT NULL, -- name of the subtype + abook_id integer NOT NULL REFERENCES TABLE_PREFIXcarddav_addressbooks (id) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE (typename,subtype,abook_id) +); + +CREATE SEQUENCE TABLE_PREFIXcarddav_groups_seq + INCREMENT BY 1 + NO MAXVALUE + MINVALUE 1 + CACHE 1; + +CREATE TABLE TABLE_PREFIXcarddav_groups ( + id integer DEFAULT nextval('TABLE_PREFIXcarddav_groups_seq'::text) PRIMARY KEY, + abook_id integer NOT NULL REFERENCES TABLE_PREFIXcarddav_addressbooks (id) ON DELETE CASCADE ON UPDATE CASCADE, + name VARCHAR(255) NOT NULL, -- display name + vcard TEXT NOT NULL, -- complete vcard + etag VARCHAR(255) NOT NULL, -- entity tag, can be used to check if card changed on server + uri VARCHAR(255) NOT NULL, -- path of the card on the server + cuid VARCHAR(255) NOT NULL, -- unique identifier of the card within the collection + + UNIQUE(uri,abook_id), + UNIQUE(cuid,abook_id) +); + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_group_user ( + group_id integer NOT NULL, + contact_id integer NOT NULL, + + PRIMARY KEY(group_id,contact_id), + FOREIGN KEY(group_id) REFERENCES TABLE_PREFIXcarddav_groups(id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(contact_id) REFERENCES TABLE_PREFIXcarddav_contacts(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE SEQUENCE TABLE_PREFIXcarddav_migrations_seq + INCREMENT BY 1 + NO MAXVALUE + MINVALUE 1 + CACHE 1; + +CREATE TABLE TABLE_PREFIXcarddav_migrations ( + ID integer DEFAULT nextval('TABLE_PREFIXcarddav_migrations_seq'::text) PRIMARY KEY, + filename VARCHAR(64) NOT NULL, + processed_at TIMESTAMP NOT NULL DEFAULT now(), + + UNIQUE(filename) +); diff --git a/plugins/carddav/dbmigrations/0000-dbinit/sqlite3.sql b/plugins/carddav/dbmigrations/0000-dbinit/sqlite3.sql new file mode 100644 index 0000000..154e049 --- /dev/null +++ b/plugins/carddav/dbmigrations/0000-dbinit/sqlite3.sql @@ -0,0 +1,89 @@ +-- table to store the finished migrations +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_migrations ( + ID integer NOT NULL PRIMARY KEY, + filename VARCHAR(64) NOT NULL, + processed_at TIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + UNIQUE(filename) +); + +-- table to store the configured address books +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_addressbooks ( + id integer NOT NULL PRIMARY KEY, + name VARCHAR(64) NOT NULL, + username VARCHAR(64) NOT NULL, + password VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + active TINYINT UNSIGNED NOT NULL DEFAULT 1, + user_id integer NOT NULL, + last_updated DATETIME NOT NULL DEFAULT 0, -- time stamp of the last update of the local database + refresh_time TIME NOT NULL DEFAULT '01:00:00', -- time span after that the local database will be refreshed + sync_token VARCHAR(255) NOT NULL DEFAULT '', -- sync-token the server sent us for the last sync + authentication_scheme VARCHAR(64) NOT NULL DEFAULT "auto", -- the HTTP authentication scheme to use, auto will be overwritten + + presetname VARCHAR(64), -- presetname + + -- not enforced by sqlite < 3.6.19 + FOREIGN KEY(user_id) REFERENCES TABLE_PREFIXusers(user_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_contacts ( + id integer NOT NULL PRIMARY KEY, + abook_id integer NOT NULL, + name VARCHAR(255) NOT NULL, -- display name + email VARCHAR(255), -- ", " separated list of mail addresses + firstname VARCHAR(255), + surname VARCHAR(255), + organization VARCHAR(255), + showas VARCHAR(32) NOT NULL DEFAULT '', -- special display type (e.g., as a company) + vcard TEXT NOT NULL, -- complete vcard + etag VARCHAR(255) NOT NULL, -- entity tag, can be used to check if card changed on server + uri VARCHAR(255) NOT NULL, -- path of the card on the server + cuid VARCHAR(255) NOT NULL, -- unique identifier of the card within the collection + + UNIQUE(uri,abook_id), + UNIQUE(cuid,abook_id), + + -- not enforced by sqlite < 3.6.19 + FOREIGN KEY(abook_id) REFERENCES TABLE_PREFIXcarddav_addressbooks(id) ON DELETE CASCADE ON UPDATE CASCADE +); +CREATE INDEX TABLE_PREFIXcarddav_contacts_abook_id_idx ON TABLE_PREFIXcarddav_contacts(abook_id); + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_xsubtypes ( + id integer NOT NULL PRIMARY KEY, + typename VARCHAR(128) NOT NULL, -- name of the type + subtype VARCHAR(128) NOT NULL, -- name of the subtype + abook_id integer NOT NULL, + + -- not enforced by sqlite < 3.6.19 + FOREIGN KEY(abook_id) REFERENCES TABLE_PREFIXcarddav_addressbooks(id) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE(typename,subtype,abook_id) +); + + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_groups ( + id integer NOT NULL PRIMARY KEY, + abook_id integer NOT NULL, + name VARCHAR(255) NOT NULL, -- display name + vcard TEXT NOT NULL, -- complete vcard + etag VARCHAR(255) NOT NULL, -- entity tag, can be used to check if card changed on server + uri VARCHAR(255) NOT NULL, -- path of the card on the server + cuid VARCHAR(255) NOT NULL, -- unique identifier of the card within the collection + + UNIQUE(uri,abook_id), + UNIQUE(cuid,abook_id), + + -- not enforced by sqlite < 3.6.19 + FOREIGN KEY(abook_id) REFERENCES TABLE_PREFIXcarddav_addressbooks(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_group_user ( + group_id integer NOT NULL, + contact_id integer NOT NULL, + + PRIMARY KEY(group_id,contact_id), + + -- not enforced by sqlite < 3.6.19 + FOREIGN KEY(group_id) REFERENCES TABLE_PREFIXcarddav_groups(id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(contact_id) REFERENCES TABLE_PREFIXcarddav_contacts(id) ON DELETE CASCADE ON UPDATE CASCADE +); diff --git a/plugins/carddav/dbmigrations/0000-sample/mysql.sql b/plugins/carddav/dbmigrations/0000-sample/mysql.sql new file mode 100644 index 0000000..8045873 --- /dev/null +++ b/plugins/carddav/dbmigrations/0000-sample/mysql.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- Just an example migration. diff --git a/plugins/carddav/dbmigrations/0000-sample/postgres.sql b/plugins/carddav/dbmigrations/0000-sample/postgres.sql new file mode 100644 index 0000000..8045873 --- /dev/null +++ b/plugins/carddav/dbmigrations/0000-sample/postgres.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- Just an example migration. diff --git a/plugins/carddav/dbmigrations/0000-sample/sqlite3.sql b/plugins/carddav/dbmigrations/0000-sample/sqlite3.sql new file mode 100644 index 0000000..8045873 --- /dev/null +++ b/plugins/carddav/dbmigrations/0000-sample/sqlite3.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- Just an example migration. diff --git a/plugins/carddav/dbmigrations/0001-categories/mysql.sql b/plugins/carddav/dbmigrations/0001-categories/mysql.sql new file mode 100644 index 0000000..b8694b0 --- /dev/null +++ b/plugins/carddav/dbmigrations/0001-categories/mysql.sql @@ -0,0 +1 @@ +ALTER TABLE TABLE_PREFIXcarddav_addressbooks ADD `use_categories` INT NOT NULL DEFAULT '0' AFTER `presetname`; diff --git a/plugins/carddav/dbmigrations/0001-categories/postgres.sql b/plugins/carddav/dbmigrations/0001-categories/postgres.sql new file mode 100644 index 0000000..4c71b47 --- /dev/null +++ b/plugins/carddav/dbmigrations/0001-categories/postgres.sql @@ -0,0 +1 @@ +ALTER TABLE TABLE_PREFIXcarddav_addressbooks ADD COLUMN use_categories SMALLINT NOT NULL DEFAULT 0; diff --git a/plugins/carddav/dbmigrations/0001-categories/sqlite3.sql b/plugins/carddav/dbmigrations/0001-categories/sqlite3.sql new file mode 100644 index 0000000..38ed08a --- /dev/null +++ b/plugins/carddav/dbmigrations/0001-categories/sqlite3.sql @@ -0,0 +1 @@ +ALTER TABLE TABLE_PREFIXcarddav_addressbooks ADD use_categories TINYINT NOT NULL DEFAULT 0; diff --git a/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/mysql.sql b/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/mysql.sql new file mode 100644 index 0000000..a16d718 --- /dev/null +++ b/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/mysql.sql @@ -0,0 +1,2 @@ +ALTER TABLE TABLE_PREFIXcarddav_addressbooks MODIFY COLUMN `username` VARCHAR(255); +ALTER TABLE TABLE_PREFIXcarddav_addressbooks MODIFY COLUMN `presetname` VARCHAR(255); diff --git a/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/postgres.sql b/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/postgres.sql new file mode 100644 index 0000000..b89a794 --- /dev/null +++ b/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/postgres.sql @@ -0,0 +1,2 @@ +ALTER TABLE TABLE_PREFIXcarddav_addressbooks ALTER COLUMN username TYPE VARCHAR(255); +ALTER TABLE TABLE_PREFIXcarddav_addressbooks ALTER COLUMN presetname TYPE VARCHAR(255); diff --git a/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/sqlite3.sql b/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/sqlite3.sql new file mode 100644 index 0000000..d65c719 --- /dev/null +++ b/plugins/carddav/dbmigrations/0002-increasetextfieldlengths/sqlite3.sql @@ -0,0 +1,32 @@ +PRAGMA foreign_keys=OFF; + +CREATE TABLE IF NOT EXISTS TABLE_PREFIXcarddav_addressbooks_X ( + id integer NOT NULL PRIMARY KEY, + name VARCHAR(64) NOT NULL, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + url VARCHAR(255) NOT NULL, + active TINYINT UNSIGNED NOT NULL DEFAULT 1, + user_id integer NOT NULL, + last_updated DATETIME NOT NULL DEFAULT 0, -- time stamp of the last update of the local database + refresh_time TIME NOT NULL DEFAULT '01:00:00', -- time span after that the local database will be refreshed + sync_token VARCHAR(255) NOT NULL DEFAULT '', -- sync-token the server sent us for the last sync + authentication_scheme VARCHAR(64) NOT NULL DEFAULT "auto", -- the HTTP authentication scheme to use, auto will be overwritten + + presetname VARCHAR(255), -- presetname + + use_categories TINYINT NOT NULL DEFAULT 0, + + -- not enforced by sqlite < 3.6.19 + FOREIGN KEY(user_id) REFERENCES TABLE_PREFIXusers(user_id) ON DELETE CASCADE ON UPDATE CASCADE +); + +INSERT INTO TABLE_PREFIXcarddav_addressbooks_X SELECT * FROM TABLE_PREFIXcarddav_addressbooks; + +DROP TABLE TABLE_PREFIXcarddav_addressbooks; + +ALTER TABLE TABLE_PREFIXcarddav_addressbooks_X RENAME TO TABLE_PREFIXcarddav_addressbooks; + +PRAGMA foreign_key_check; + +PRAGMA foreign_keys=ON; diff --git a/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/README.md b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/README.md new file mode 100644 index 0000000..d08d7de --- /dev/null +++ b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/README.md @@ -0,0 +1 @@ +This migration has been disabled because it was broken. It got fixed and replaced by 0004-fixtimestampdefaultvalue. diff --git a/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/mysql.sql b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/mysql.sql new file mode 100644 index 0000000..8045873 --- /dev/null +++ b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/mysql.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- Just an example migration. diff --git a/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/postgres.sql b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/postgres.sql new file mode 100644 index 0000000..977d8bd --- /dev/null +++ b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/postgres.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- No migration diff --git a/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/sqlite3.sql b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/sqlite3.sql new file mode 100644 index 0000000..977d8bd --- /dev/null +++ b/plugins/carddav/dbmigrations/0003-fixtimestampdefaultvalue/sqlite3.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- No migration diff --git a/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/mysql.sql b/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/mysql.sql new file mode 100644 index 0000000..9ae5f33 --- /dev/null +++ b/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/mysql.sql @@ -0,0 +1 @@ +ALTER TABLE TABLE_PREFIXcarddav_addressbooks MODIFY COLUMN `last_updated` TIMESTAMP NOT NULL DEFAULT '2000-01-01 00:00:01'; diff --git a/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/postgres.sql b/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/postgres.sql new file mode 100644 index 0000000..977d8bd --- /dev/null +++ b/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/postgres.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- No migration diff --git a/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/sqlite3.sql b/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/sqlite3.sql new file mode 100644 index 0000000..977d8bd --- /dev/null +++ b/plugins/carddav/dbmigrations/0004-fixtimestampdefaultvalue/sqlite3.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- No migration diff --git a/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/mysql.sql b/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/mysql.sql new file mode 100644 index 0000000..5a3eaf8 --- /dev/null +++ b/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/mysql.sql @@ -0,0 +1,56 @@ +-- table to store the configured address books +ALTER TABLE TABLE_PREFIXcarddav_addressbooks DROP INDEX `user_id`, ADD UNIQUE `user_id` (`user_id`, `presetname`(191)) USING BTREE; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `name` `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `username` `username` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `password` `password` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `url` `url` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `sync_token` `sync_token` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `authentication_scheme` `authentication_scheme` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_addressbooks CHANGE `presetname` `presetname` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER TABLE TABLE_PREFIXcarddav_contacts DROP INDEX `uri`, ADD UNIQUE `uri` (`uri`(191), `abook_id`) USING BTREE; +ALTER TABLE TABLE_PREFIXcarddav_contacts DROP INDEX `cuid`, ADD UNIQUE `cuid` (`cuid`(191), `abook_id`) USING BTREE; +ALTER TABLE TABLE_PREFIXcarddav_contacts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `name` `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `email` `email` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `firstname` `firstname` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `surname` `surname` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `organization` `organization` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `showas` `showas` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `vcard` `vcard` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `etag` `etag` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `uri` `uri` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_contacts CHANGE `cuid` `cuid` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER TABLE TABLE_PREFIXcarddav_groups DROP INDEX `uri`, ADD UNIQUE `uri` (`uri`(191), `abook_id`) USING BTREE; +ALTER TABLE TABLE_PREFIXcarddav_groups DROP INDEX `cuid`, ADD UNIQUE `cuid` (`cuid`(191), `abook_id`) USING BTREE; +ALTER TABLE TABLE_PREFIXcarddav_groups CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_groups CHANGE `name` `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_groups CHANGE `etag` `etag` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_groups CHANGE `uri` `uri` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_groups CHANGE `cuid` `cuid` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER TABLE TABLE_PREFIXcarddav_group_user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER TABLE TABLE_PREFIXcarddav_migrations CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_migrations CHANGE `filename` `filename` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +ALTER TABLE TABLE_PREFIXcarddav_xsubtypes CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_xsubtypes CHANGE `typename` `typename` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +ALTER TABLE TABLE_PREFIXcarddav_xsubtypes CHANGE `subtype` `subtype` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + + +REPAIR TABLE TABLE_PREFIXcarddav_addressbooks; +REPAIR TABLE TABLE_PREFIXcarddav_contacts; +REPAIR TABLE TABLE_PREFIXcarddav_groups; +REPAIR TABLE TABLE_PREFIXcarddav_group_user; +REPAIR TABLE TABLE_PREFIXcarddav_migrations; +REPAIR TABLE TABLE_PREFIXcarddav_xsubtypes; + +OPTIMIZE TABLE TABLE_PREFIXcarddav_addressbooks; +OPTIMIZE TABLE TABLE_PREFIXcarddav_contacts; +OPTIMIZE TABLE TABLE_PREFIXcarddav_groups; +OPTIMIZE TABLE TABLE_PREFIXcarddav_group_user; +OPTIMIZE TABLE TABLE_PREFIXcarddav_migrations; +OPTIMIZE TABLE TABLE_PREFIXcarddav_xsubtypes; diff --git a/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/postgres.sql b/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/postgres.sql new file mode 100644 index 0000000..977d8bd --- /dev/null +++ b/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/postgres.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- No migration diff --git a/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/sqlite3.sql b/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/sqlite3.sql new file mode 100644 index 0000000..977d8bd --- /dev/null +++ b/plugins/carddav/dbmigrations/0005-changemysqlut8toutf8mb4/sqlite3.sql @@ -0,0 +1 @@ +SELECT * FROM TABLE_PREFIXcarddav_migrations; -- No migration diff --git a/plugins/carddav/localization/cs_CZ.inc b/plugins/carddav/localization/cs_CZ.inc new file mode 100644 index 0000000..dee2599 --- /dev/null +++ b/plugins/carddav/localization/cs_CZ.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Aktivovat adresář kontaktů CardDAV", + 'cd_newabboxtitle'=> "Přidat nový adresář kontaktů CardDAV", + 'cd_username'=> "Uživatel", + 'cd_password'=> "Heslo", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "\"Nainstalovaná verze PHP je zastaralá. Aktualizujte PHP na verzi 5.6.18 nebo vyšší! V současnosti máte nainstalovanou následující verzi:", + 'cd_name'=> "Název adresáře kontaktů CardDAV", + 'cd_delete'=> "Odstranit tento adresář kontaktů", + 'cd_name_new'=> "Nastavit nový adresář kontaktů", + 'cd_refresh_time'=> "Interval aktualizace (hodiny)", + 'cd_err_noabfound'=> "no addressbook found", + 'cd_preemptive_auth'=> "Preemptively authenticate against server. Use for ownCloud!", +); +?> diff --git a/plugins/carddav/localization/de_DE.inc b/plugins/carddav/localization/de_DE.inc new file mode 100644 index 0000000..f03b275 --- /dev/null +++ b/plugins/carddav/localization/de_DE.inc @@ -0,0 +1,21 @@ + "CardDAV", + 'cd_active'=> "CardDAV-Adressbuch aktivieren", + 'cd_newabboxtitle'=> "Neues Adressbuch hinzufügen", + 'cd_username'=> "Benutzername", + 'cd_password'=> "Passwort", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Ihre PHP Version ist zu alt! Es wird mindestens 5.6.18 benoetigt! Sie haben folgende Version installiert:", + 'cd_name'=> "Name des Adressbuches", + 'cd_use_categories' => "Modernen Gruppenmechanismus verwenden\n(Alte RCMCardDAV Gruppen gehen verloren!)", + 'cd_delete'=> "Dieses Adressbuch entfernen", + 'cd_name_new'=> "Neues Adressbuch konfigurieren", + 'cd_refresh_time'=> "Aktualisierungsintervall (Stunden)", + 'cd_err_noabfound'=> "Kein Adressbuch gefunden.", + 'cd_preemptive_auth'=> "Immer am Server authentifizieren. Für ownCloud aktivieren!", + 'cd_enabled' => 'Aktiv', + 'cd_disabled' => 'Inaktiv', + 'cd_frompreset' => ' (aus Vorgabe _PRESETNAME_)', +); +?> diff --git a/plugins/carddav/localization/en_US.inc b/plugins/carddav/localization/en_US.inc new file mode 100644 index 0000000..f09b2cb --- /dev/null +++ b/plugins/carddav/localization/en_US.inc @@ -0,0 +1,21 @@ + 'CardDAV', + 'cd_active' => 'Activate CardDAV-Addressbook', + 'cd_newabboxtitle'=> "Add new addressbook", + 'cd_username' => 'Username', + 'cd_password' => 'Password', + 'cd_url' => 'URL', + 'cd_php_too_old' => "Your version of PHP is too old! Please update to at least 5.6.18! You got the following version installed:", + 'cd_name' => "Name of the addressbook", + 'cd_use_categories' => "Use modern group mechanism\n(you will lose old RCMCardDAV groups!)", + 'cd_delete' => "Remove this addressbook", + 'cd_name_new' => "Configure new addressbook", + 'cd_refresh_time' => "Update interval (hours)", + 'cd_err_noabfound' => 'no addressbook found', + 'cd_preemptive_auth' => 'Preemptively authenticate against server. Use for ownCloud!', + 'cd_enabled' => 'Enabled', + 'cd_disabled' => 'Disabled', + 'cd_frompreset' => ' (from preset _PRESETNAME_)', +); +?> diff --git a/plugins/carddav/localization/es_ES.inc b/plugins/carddav/localization/es_ES.inc new file mode 100644 index 0000000..98f118a --- /dev/null +++ b/plugins/carddav/localization/es_ES.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Activar Libreta de Direcciones CardDAV", + 'cd_newabboxtitle'=> "Añadir nueva Libreta de Direcciones", + 'cd_username'=> "Nombre de usuario", + 'cd_password'=> "Contraseña", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "La versión de PHP es demasiado antigua! Por favor, actualiza al menos a la 5.6.18! La versión instalada es la siguiente:", + 'cd_name'=> "Nombre de la libreta de direcciones", + 'cd_delete'=> "Eliminar esta libreta de direcciones", + 'cd_name_new'=> "Configurar una nueva libreta de direcciones", + 'cd_refresh_time'=> "Intervalo de actualización (horas)", + 'cd_err_noabfound'=> "libreta de direcciones no encontrada", + 'cd_preemptive_auth'=> "Autenticar previamente contra el servidor. Necesario para OwnCloud!", +); +?> diff --git a/plugins/carddav/localization/fr_FR.inc b/plugins/carddav/localization/fr_FR.inc new file mode 100644 index 0000000..2be0a51 --- /dev/null +++ b/plugins/carddav/localization/fr_FR.inc @@ -0,0 +1,20 @@ + "CardDAV", + 'cd_active'=> "Activer ce carnet d'adresses", + 'cd_newabboxtitle'=> "Ajouter un nouveau carnet d'adresses", + 'cd_username'=> "Nom d'utilisateur", + 'cd_password'=> "Mot de passe", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Votre version de PHP est trop ancienne. Veuillez mettre à jour avec la version 5.6.18 minimum! La version actuellement installée est :", + 'cd_name'=> "Nom du carnet d'adresses", + 'cd_use_categories' => "Utilise les categories comme des groupes", + 'cd_delete'=> "Supprimer ce carnet d'adresses", + 'cd_name_new'=> "Ajouter un nouveau carnet d'adresses", + 'cd_refresh_time'=> "Validité du cache local (en heures)", + 'cd_err_noabfound'=> "Aucun carnet d'adresses trouvé", + 'cd_preemptive_auth'=> "Preemptively authenticate against server. Use for ownCloud!", + 'cd_enabled'=>'Activé', + 'cd_disabled'=>'Désactivé', +); +?> diff --git a/plugins/carddav/localization/hu_HU.inc b/plugins/carddav/localization/hu_HU.inc new file mode 100644 index 0000000..f085e00 --- /dev/null +++ b/plugins/carddav/localization/hu_HU.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "CardDAV címjegyzék aktiválása", + 'cd_newabboxtitle'=> "Add new addressbook", + 'cd_username'=> "Felhasználónév", + 'cd_password'=> "Jelszó", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "A PHP verziód túl régi, e program futásához legalább 5.6.18-ás verzióra van szükség. A jelenlegi PHP verzió:", + 'cd_name'=> "Címjegyzék neve", + 'cd_delete'=> "Címjegyzék törlése", + 'cd_name_new'=> "Címjegyzék beállításai", + 'cd_refresh_time'=> "Frissítési intervallum (óra)", + 'cd_err_noabfound'=> "no addressbook found", + 'cd_preemptive_auth'=> "Preemptively authenticate against server. Use for ownCloud!", +); +?> diff --git a/plugins/carddav/localization/id_ID.inc b/plugins/carddav/localization/id_ID.inc new file mode 100644 index 0000000..5c81664 --- /dev/null +++ b/plugins/carddav/localization/id_ID.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Aktifkan buku alamat berbasis CardDAV", + 'cd_newabboxtitle'=> "Add new addressbook", + 'cd_username'=> "Nama pengguna", + 'cd_password'=> "Kata sandi", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Versi PHP yang digunakan adalah versi yang terlalu tua! Mohon di-update sedikitnya ke versi 5.6.18! Versi PHP yang digunakan oleh Anda:", + 'cd_name'=> "Nama buku alamat", + 'cd_delete'=> "Menghapuskan buku alamat ini", + 'cd_name_new'=> "Mengkonfigurasi buku alamat baru", + 'cd_refresh_time'=> "Interval update (dalam jam)", + 'cd_err_noabfound'=> "no addressbook found", + 'cd_preemptive_auth'=> "Preemptively authenticate against server. Use for ownCloud!", +); +?> diff --git a/plugins/carddav/localization/it_IT.inc b/plugins/carddav/localization/it_IT.inc new file mode 100644 index 0000000..e9e96fb --- /dev/null +++ b/plugins/carddav/localization/it_IT.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Utilizza rubrica CardDAV", + 'cd_newabboxtitle'=> "Aggiungi una nuova Rubrica", + 'cd_username'=> "Nome utente", + 'cd_password'=> "Password", + 'cd_url'=> "Indirizzo server (URL)", + 'cd_php_too_old'=> "E' necessario aggiornare PHP alla versione 5.6.18 o superiore! Attualmente hai la versione:", + 'cd_name'=> "Nome nuova rubrica", + 'cd_delete'=> "Elimina rubrica", + 'cd_name_new'=> "Crea una nuova rubrica", + 'cd_refresh_time'=> "Frequenza di aggiornamento (ore)", + 'cd_err_noabfound'=> "Nessuna Rubrica Trovata", + 'cd_preemptive_auth'=> "Preemptively authenticate against server. Use for ownCloud!", +); +?> diff --git a/plugins/carddav/localization/pl_PL.inc b/plugins/carddav/localization/pl_PL.inc new file mode 100644 index 0000000..8be5b2f --- /dev/null +++ b/plugins/carddav/localization/pl_PL.inc @@ -0,0 +1,18 @@ + "CardDAV", + 'cd_active'=> "Aktywuj książkę adresową CardDAV", + 'cd_newabboxtitle'=> "Dodaj nową książkę adresową", + 'cd_username'=> "Nazwa użytkownika", + 'cd_password'=> "Hasło", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Twoja wersja PHP jest nieaktualna! Proszę zaktualizuj do minimum wersji 5.6.18! Twoja wersja to:", + 'cd_name'=> "Nazwa książki adresowej", + 'cd_use_categories' => "Użyj nowego mechanizmu grup\n(stracisz stare grupy RCMCardDAV!)", + 'cd_delete'=> "Usuń książkę adresową", + 'cd_name_new'=> "Konfiguracja nowej książki adresowej", + 'cd_refresh_time'=> "Interwał aktualizacji (godziny)", + 'cd_err_noabfound'=> "nie znaleziono książek adresowych", + 'cd_preemptive_auth'=> "Zapobiegawczo uwierzytelnij z serwerem. Używaj dla ownCloud!", +); +?> diff --git a/plugins/carddav/localization/ru_RU.inc b/plugins/carddav/localization/ru_RU.inc new file mode 100644 index 0000000..278f14b --- /dev/null +++ b/plugins/carddav/localization/ru_RU.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Активировать адресную книгу CardDAV", + 'cd_newabboxtitle'=> "Добавить новую адресную книгу", + 'cd_username'=> "Имя пользователя", + 'cd_password'=> "Пароль", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Ваша версия PHP слишком устарела! Пожалуйста, обновитесь по крайней мере до 5.6.18! У Вас установлена следующая версия: ", + 'cd_name'=> "Название адресной книги", + 'cd_delete'=> "Удалить адресную книгу", + 'cd_name_new'=> "Настроить новую адресную книгу", + 'cd_refresh_time'=> "Частота обновления (часов)", + 'cd_err_noabfound'=> "Не найдено ни одной адресной книги", + 'cd_preemptive_auth'=> "Предварительно авторизоваться на сервере. Используйте с ownCloud!", +); +?> diff --git a/plugins/carddav/localization/sv_SE.inc b/plugins/carddav/localization/sv_SE.inc new file mode 100644 index 0000000..7b11f65 --- /dev/null +++ b/plugins/carddav/localization/sv_SE.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Aktivera CardDAV-adressboken", + 'cd_newabboxtitle'=> "Add new addressbook", + 'cd_username'=> "Användarnamn", + 'cd_password'=> "Lösenord", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Din version av PHP är för gammal! Vänligen uppdatera till åtminstone 5.0.3! Du har följande version installerad: ", + 'cd_name'=> "Namn på adressboken", + 'cd_delete'=> "Radera denna adressbok", + 'cd_name_new'=> "Konfigurera en ny addressbok", + 'cd_refresh_time'=> "Uppdateringsintervall (timmar)", + 'cd_err_noabfound'=> "no addressbook found", + 'cd_preemptive_auth'=> "Preemptively authenticate against server. Use for ownCloud!", +); +?> diff --git a/plugins/carddav/localization/uk_UK.inc b/plugins/carddav/localization/uk_UK.inc new file mode 100644 index 0000000..bcafeb6 --- /dev/null +++ b/plugins/carddav/localization/uk_UK.inc @@ -0,0 +1,17 @@ + "CardDAV", + 'cd_active'=> "Увімкнути список контактів CardDAV", + 'cd_newabboxtitle'=> "Додати новий список контактів", + 'cd_username'=> "Ім'я користувача", + 'cd_password'=> "Пароль", + 'cd_url'=> "URL", + 'cd_php_too_old'=> "Ваша версія PHP занадто стара! Будь ласка, оновіть її принаймні до 5.6.18! У Вас встановлена така версія:", + 'cd_name'=> "Назва списку контактів", + 'cd_delete'=> "Видалити цей список контактів", + 'cd_name_new'=> "Налаштувати цей список контактів", + 'cd_refresh_time'=> "Період оновлення (в годинах)", + 'cd_err_noabfound'=> "не знайдено жодного списку контактів", + 'cd_preemptive_auth'=> "Попередньо авторизуватись на сервері. Використовуйте для ownCloud!", +); +?> diff --git a/plugins/carddav/package.xml b/plugins/carddav/package.xml new file mode 100644 index 0000000..f15d618 --- /dev/null +++ b/plugins/carddav/package.xml @@ -0,0 +1,75 @@ + + + carddav + www.benjamin-schieder.de + CardDAV plugin for Roundcube + + Adds functionality to use, synchronize and manipulate CardDAV accounts with Roundcube. + Tested with davical, owncloud, MacOS Addressbook and others. + + + Benjamin Schieder + blindcoder + carddav@wegwerf.anderdonau.de + yes + + + Michael Stilkerich + mike2k + ms@mike2k.de + yes + + 2018-10-01 + + 3.0.3 + + + stable + + GNU GPLv2 + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5.6.18 + + + + + diff --git a/plugins/carddav/skins/classic/carddav.css b/plugins/carddav/skins/classic/carddav.css new file mode 100644 index 0000000..95f0652 --- /dev/null +++ b/plugins/carddav/skins/classic/carddav.css @@ -0,0 +1,6 @@ +#sections-table #rcmrowcd_preferences td.section { + background-position: 6px -766px; +} +#sections-table #rcmrowcd_preferences.selected td.section { + background-position: 6px -791px; +} diff --git a/plugins/carddav/skins/elastic/carddav.css b/plugins/carddav/skins/elastic/carddav.css new file mode 100644 index 0000000..67c591b --- /dev/null +++ b/plugins/carddav/skins/elastic/carddav.css @@ -0,0 +1,3 @@ +.listing.iconized tr.cd_preferences>td.section:before { + content: "\f47f" +} diff --git a/plugins/carddav/skins/larry/carddav.css b/plugins/carddav/skins/larry/carddav.css new file mode 100644 index 0000000..95f0652 --- /dev/null +++ b/plugins/carddav/skins/larry/carddav.css @@ -0,0 +1,6 @@ +#sections-table #rcmrowcd_preferences td.section { + background-position: 6px -766px; +} +#sections-table #rcmrowcd_preferences.selected td.section { + background-position: 6px -791px; +} diff --git a/plugins/managesieve/config.inc.php b/plugins/managesieve/config.inc.php new file mode 100644 index 0000000..1d5d379 --- /dev/null +++ b/plugins/managesieve/config.inc.php @@ -0,0 +1,110 @@ + array( +// 'verify_peer' => true, +// 'verify_depth' => 3, +// 'cafile' => '/etc/openssl/certs/ca.crt', +// ), +// ); +// Note: These can be also specified as an array of options indexed by hostname +$config['managesieve_conn_options'] = null; + +// default contents of filters script (eg. default spam filter) +$config['managesieve_default'] = '/etc/dovecot/sieve/global'; + +// The name of the script which will be used when there's no user script +$config['managesieve_script_name'] = 'managesieve'; + +// Sieve RFC says that we should use UTF-8 endcoding for mailbox names, +// but some implementations does not covert UTF-8 to modified UTF-7. +// Defaults to UTF7-IMAP +$config['managesieve_mbox_encoding'] = 'UTF-8'; + +// I need this because my dovecot (with listescape plugin) uses +// ':' delimiter, but creates folders with dot delimiter +$config['managesieve_replace_delimiter'] = ''; + +// disabled sieve extensions (body, copy, date, editheader, encoded-character, +// envelope, environment, ereject, fileinto, ihave, imap4flags, index, +// mailbox, mboxmetadata, regex, reject, relational, servermetadata, +// spamtest, spamtestplus, subaddress, vacation, variables, virustest, etc. +// Note: not all extensions are implemented +$config['managesieve_disabled_extensions'] = array(); + +// Enables debugging of conversation with sieve server. Logs it into /sieve +$config['managesieve_debug'] = false; + +// Enables features described in http://wiki.kolab.org/KEP:14 +$config['managesieve_kolab_master'] = false; + +// Script name extension used for scripts including. Dovecot uses '.sieve', +// Cyrus uses '.siv'. Doesn't matter if you have managesieve_kolab_master disabled. +$config['managesieve_filename_extension'] = '.sieve'; + +// List of reserved script names (without extension). +// Scripts listed here will be not presented to the user. +$config['managesieve_filename_exceptions'] = array(); + +// List of domains limiting destination emails in redirect action +// If not empty, user will need to select domain from a list +$config['managesieve_domains'] = array(); + +// Enables separate management interface for vacation responses (out-of-office) +// 0 - no separate section (default), +// 1 - add Vacation section, +// 2 - add Vacation section, but hide Filters section +$config['managesieve_vacation'] = 0; + +// Default vacation interval (in days). +// Note: If server supports vacation-seconds extension it is possible +// to define interval in seconds here (as a string), e.g. "3600s". +$config['managesieve_vacation_interval'] = 0; + +// Some servers require vacation :addresses to be filled with all +// user addresses (aliases). This option enables automatic filling +// of these on initial vacation form creation. +$config['managesieve_vacation_addresses_init'] = false; + +// Sometimes you want to always reply with mail email address +// This option enables automatic filling of :from field on initial vacation form creation. +$config['managesieve_vacation_from_init'] = false; + +// Supported methods of notify extension. Default: 'mailto' +$config['managesieve_notify_methods'] = array('mailto'); + +// Enables scripts RAW editor feature +$config['managesieve_raw_editor'] = true; diff --git a/roundcube.config.php b/roundcube.config.php new file mode 100644 index 0000000..1dab1ec --- /dev/null +++ b/roundcube.config.php @@ -0,0 +1,101 @@ + 'toptica', + 'ssl://imap.gmx.net' => 'GMX', + 'ssl://imap.web.de' => 'Web', +); + +// SMTP server host (for sending mails). +// To use SSL/TLS connection, enter hostname with prefix ssl:// or tls:// +// If left blank, the PHP mail() function is used +// Supported replacement variables: +// %h - user's IMAP hostname +// %n - hostname ($_SERVER['SERVER_NAME']) +// %t - hostname without the first part +// %d - domain (http hostname $_SERVER['HTTP_HOST'] without the first part) +// %z - IMAP domain (IMAP hostname without the first part) +// For example %n = mail.domain.tld, %t = domain.tld +//$config['smtp_server'] = 'mail.lima-city.de'; + +// SMTP port (default is 25; use 587 for STARTTLS or 465 for the +// deprecated SSL over SMTP (aka SMTPS)) +$config['smtp_port'] = 25; + +// SMTP username (if required) if you use %u as the username Roundcube +// will use the current username for login +// $config['smtp_user'] = ''; + +// SMTP password (if required) if you use %p as the password Roundcube +// will use the current user's password for login +// $config['smtp_pass'] = ''; + +// provide an URL where a user can get support for this Roundcube installation +// PLEASE DO NOT LINK TO THE ROUNDCUBE.NET WEBSITE HERE! +//$config['support_url'] = 'https://web.hilie.de/roundcube'; + +// Name your service. This is displayed on the login screen and in the window title +$config['product_name'] = 'Roundcube Webmail'; + +// this key is used to encrypt the users imap password which is stored +// in the session record (and the client cookie if remember password is enabled). +// please provide a string of exactly 24 chars. +// YOUR KEY MUST BE DIFFERENT THAN THE SAMPLE VALUE FOR SECURITY REASONS +$config['des_key'] = 'sv5KuHfUagjggBU2KzXI2Uo'; +// $config['des_key'] = 'Change_me_I_am_example!!'; + +// List of active plugins (in plugins/ directory) +$config['plugins'] = array( + 'archive', + 'zipdownload', +// 'managesieve', +// 'identity_smtp', +// 'carddav', + 'newmail_notifier', + // 'html5_notifier', +); + +// skin name: folder from skins/ +// $config['skin'] = 'larry'; + +//$config['debug_level'] = 4; +//$config['imap_debug'] = true; +$config['imap_cache'] = 'db'; +$config['messages_cache'] = true; +$config['ip_check'] = false;