pax_global_header00006660000000000000000000000064136234637520014525gustar00rootroot0000000000000052 comment=4b383b2371020303a564c110f24270567ce7375e DAV-4-TbSync-1.8/000077500000000000000000000000001362346375200133505ustar00rootroot00000000000000DAV-4-TbSync-1.8/.github/000077500000000000000000000000001362346375200147105ustar00rootroot00000000000000DAV-4-TbSync-1.8/.github/issue_template.md000066400000000000000000000010631362346375200202550ustar00rootroot00000000000000## Your environment TbSync version: DAV-4-TbSync version: Thunderbird version: [ ] Yes, I have installed the latest available (beta) version from - https://github.com/jobisoft/TbSync/releases - https://github.com/jobisoft/DAV-4-TbSync/releases and my issue is not yet fixed, I can still reproduce it. ## Expected behavior ... ## Actual behavior ... ## Steps to reproduce ... To help resolving your issue, enable debug logging (TbSync Account Manager -> Help) and send me the debug.log via e-mail (use the title of your issue as subject of the email). DAV-4-TbSync-1.8/.gitignore000066400000000000000000000000061362346375200153340ustar00rootroot00000000000000*.xpi DAV-4-TbSync-1.8/CONTRIBUTORS.md000066400000000000000000000005051362346375200156270ustar00rootroot00000000000000## Creator * John Bieling ## Contributors * John Bieling * Brad2014 * Nathan Gauër * Alexandr Heymdall (vCard parser) ## Translators * Ettore Atalan (de) * John Bieling (de, en-US) * Wanderlei Hüttel (pt-BR) * Alessandro Menti (it) * Óvári (hu) * Alexey Sinitsyn (ru) * Jérémie Parisel (fr) * Daniel Wróblewski (pl) DAV-4-TbSync-1.8/LICENSE000066400000000000000000000405251362346375200143630ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. DAV-4-TbSync-1.8/Makebeta000066400000000000000000000026141362346375200150070ustar00rootroot00000000000000#!/bin/bash # This file is part of DAV-4-TbSync. # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # $1 link to base web server : https://tbsync.jobisoft.de/beta # $2 local path of base web server : /var/www/jobisoft.de/tbsync/beta # $3 name of XPI : DAV-4-TbSync.xpi git clean -df git checkout -- . git pull version=$(cat manifest.json | jq -r .version) updatefile=update-dav.json sed -i "s/%VERSION%/$version/g" "beta-release-channel-update.json" sed -i "s|%LINK%|$1/$3|g" "beta-release-channel-update.json" sed -i "s/static getApiVersion() { return \"/static getApiVersion() { return \"Beta /g" "content/provider.js" echo "Releasing version $version via beta release channel (will include updateURL)" sed -i "s|\"name\": \"__MSG_extensionName__\",|\"name\": \"DAV for TbSync [Beta Release Channel]\",|g" "manifest.json" sed -i "s|\"gecko\": {|\"gecko\": {\n \"update_url\": \"$1/$updatefile\",|g" "manifest.json" cp beta-release-channel-update.json $2/$updatefile rm -f $3 rm -f $3.tar.gz zip -r $3 content _locales skin chrome.manifest manifest.json bootstrap.js LICENSE CONTRIBUTORS.md tar cfvz $3.tar.gz content _locales skin chrome.manifest manifest.json bootstrap.js LICENSE CONTRIBUTORS.md cp $3 $2/$3 cp $3.tar.gz $2/$3.tar.gz DAV-4-TbSync-1.8/Makefile.bat000066400000000000000000000006571362346375200155650ustar00rootroot00000000000000@echo off REM This file is part of DAV-4-TbSync. REM REM This Source Code Form is subject to the terms of the Mozilla Public REM License, v. 2.0. If a copy of the MPL was not distributed with this REM file, You can obtain one at http://mozilla.org/MPL/2.0/. del DAV-4-TbSync.xpi "C:\Program Files\7-Zip\7zG.exe" a -tzip DAV-4-TbSync.xpi content _locales skin chrome.manifest manifest.json bootstrap.js LICENSE CONTRIBUTORS.md DAV-4-TbSync-1.8/README.md000066400000000000000000000041511362346375200146300ustar00rootroot00000000000000# DAV-4-TbSync This provider add-on adds CalDAV & CardDAV synchronization capabilities to [TbSync](https://github.com/jobisoft/TbSync/). More information can be found in the [wiki](https://github.com/jobisoft/DAV-4-TbSync/wiki/About:-Provider-for-CalDAV-&-CardDAV) of this repository ## Want to add or fix a localization? To help translating this project, please visit [crowdin.com](https://crowdin.com/profile/jobisoft), where the localizations are managed. If you want to add a new language, just contact me and I will set it up. ## Icon sources and attributions #### Public Domain * posteo.de icons by [posteo.de](https://commons.wikimedia.org/wiki/File:Posteo.png) * mailbox.org icons by [mailbox.org](https://commons.wikimedia.org/wiki/File:Logo_mailbox.org_RGB_658x358.jpg) #### CC-BY 3.0 * ics16.png by [FatCow Web Hosting](https://www.iconfinder.com/icons/35803/) * google icons by [Just UI](https://www.iconfinder.com/icons/1298745/) * icloud icons by [Five Icons](https://www.iconfinder.com/icons/252111/apple_icon) * yahoo icons by [Five Icons](https://www.iconfinder.com/icons/252070/yahoo_icon) * gmx icons by [CloudSponge](https://www.iconfinder.com/icons/1175604/address_book_contact_contacts_email_gmx_square_icon) * web icons by [CloudSponge](https://www.iconfinder.com/icons/1175616/address_book_contact_contacts_email_mail_square_webde_icon) * type.pref.png by [Steve Schoger](https://www.iconfinder.com/icons/3671863/) * type.other.png by [Steve Schoger](https://www.iconfinder.com/icons/3671671/) * type.work.png by [Steve Schoger](https://www.iconfinder.com/icons/3671695/) * type.home.png by [Steve Schoger](https://www.iconfinder.com/icons/3671775/) * type.car.png by [Steve Schoger](https://www.iconfinder.com/icons/3671885/) * type.cell.png by [Steve Schoger](https://www.iconfinder.com/icons/3671810/) * type.fax.png by [Steve Schoger](https://www.iconfinder.com/icons/3671840/) * type.video.png by [Steve Schoger](https://www.iconfinder.com/icons/3671900/) * type.voice.png by [Steve Schoger](https://www.iconfinder.com/icons/3671831/) * type.pager.png by [Steve Schoger](https://www.iconfinder.com/icons/3671720/) DAV-4-TbSync-1.8/_locales/000077500000000000000000000000001362346375200151315ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/Readme.txt000066400000000000000000000003511362346375200170660ustar00rootroot00000000000000Want to add or fix a localization? To help translating this project, please visit https://crowdin.com/profile/jobisoft where the localizations are managed. If you want to add a new language, just contact me and I will set it up. DAV-4-TbSync-1.8/_locales/bg/000077500000000000000000000000001362346375200155215ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/bg/dav.dtd000066400000000000000000000074541362346375200170020ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/bg/dav.properties000066400000000000000000000200011362346375200204020ustar00rootroot00000000000000menu.name=CalDAV и CardDAV defaultname.calendar=Календар defaultname.contacts=Контакти syncstate.send.getfolders=Запитване за списък с ресурсите syncstate.eval.folders=Обновяване на списъка с ресурсите syncstate.prepare.request.localchanges=Изпращане на местните промени syncstate.send.request.localchanges=Изчакване за потвърждение, че местните промени са изпратени syncstate.eval.response.localchanges=Обработка на потвърждението за местните промени syncstate.send.request.remotechanges=Изчакване на данни от сървъра за промени syncstate.eval.response.remotechanges=Обработка на новите данни от сървъра status.networkerror=Не можах да се свръжа със сървъра. status.404=HTTP грешка 404 (поисканият ресурс не беше намерен). status.403=Връзката беше отхвърнена от сървъра (забранена). status.401=Достъпът отказан, проверете потребителското име и паролата. status.500=Непозната грешка от сървъра (HTTP грешка 500). status.503=Услугата е недостъпна. status.softerror=Пренебрегната грешка (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=Поради частично недостатъчни права за правене на промени, някои действия бяха отхвърлени от сървъра и местните промени бяха изтрити. status.malformed-xml=Отговорът не е правилен XML. Синхронизацията беше прекъсната. Проверете протокола със събитията за подробности. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=Откриването на настройките по RFC6764 за „##replace.1##“ не проработи. Въведете друг адрес на сървър или направете ръчно настройване, за да въведете сами адресите. status.rfc6764-lookup-failed=Запитването за „##replace.1##“ не намери адресите за CalDAV и CardDAV услугите. Въведете името на сървъра, за да продължи настройването. status.missing-permission=Недостатъчни права: ##replace.1## status.caldavservernotfound=Не беше намерен CalDAV сървър. status.carddavservernotfound=Не беше намерен CardDAV сървър. config.custom=CalDAV и CardDAV настройки на сървъра add.serverprofile.discovery=Автоматична настройка add.serverprofile.discovery.description=Повечето сървъри поддържат автоматично настойване, за което е нужно да въведете само електронен адрес или потребителско име и сървър. add.serverprofile.discovery.details1=Въведете за автоматичното откриване на CalDAV и CardDAV адресите вашите данни за вход и съответния сървър (н.пр. “cloud.example.bg”) add.serverprofile.discovery.details2=Ако потребителското ви име е адрес на електронна поща, указването на сървър не е задължително, тъй като информацията за настройките може да се извлече от доставчика (по RFC 6764). add.serverprofile.discovery.server-optional=по желание add.serverprofile.custom=Ръчно настройване add.serverprofile.custom.description=Адресите за CalDAV и CardDAV услугите може да се настроят ръчно. add.serverprofile.custom.details1=Необходимите CalDAV и CardDAV адреси, съответно WebDAV адреса на потребителя, ще ви ги даде вашият доставчик. add.serverprofile.custom.details2=Ако оставите един от двата адреса празни, съответната услуга ще бъде изключена за вашата регистрация. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux е услуга, която синхронизира контакти, календари и задачи. Услугата се предлага от фирмата зад sabre/dav и седалището ѝ е в Германия. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org е германски доставчик от германия с акцент върху сигурността за частни и бизнес клиенти. Предлага календари, контакти и място в облака. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=Потребителското име е вашият Apple ID. Паролата обаче не е паролата за вашия Apple ID! За да синхронизира TbSync вашите контакти и календари е необходимо да включите регистрацията с два фактора (2FA) за вашата iCloud-регистрация и да създадете отделна парола за приложението TbSync. add.serverprofile.icloud.details2=Това е допълнително ниво на защита, наложено от Apple, което не дава достъп на приложенията до вашата Apple-регистрация. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Europa) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (USA) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Гугъл add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Гугъл използва съвременен подход за удостоверяване без потребителско име и парола. След като изберете „Напред >“, ще се пръкне джам. Влезте чрез него в Гугъл и позволете на “Provider for CalDAV & CardDAV” достъп до данните ви. acl.readwrite=Права за писане на сървъра: ##replace.1## acl.readonly=Достъп до сървъра само за четене (изтрива местните промени) acl.modify=промяна acl.add=вмъквате acl.delete=изтриване acl.none=никакви add.spinner.validating=Проверяване на връзката към сървъра add.spinner.query=Изпращане на RFC 6764 запитване до „##replace.1##“ autocomplete.WORK=служебен autocomplete.HOME=личен autocomplete.PREF=предпочитан status.gContactSync=Има несъвместимост с gContactSync при включена синхронизация на групите от контакти. Изключете едно от двете, докато грешката не бъде отстранена. DAV-4-TbSync-1.8/_locales/bg/messages.json000066400000000000000000000004721362346375200202260ustar00rootroot00000000000000{ "extensionName": { "message": "Доставчик за CalDAV и CardDAV" }, "extensionDescription": { "message": "Разширява TbSync и позволява синхронизацията с CalDAV и CardDAV регистрации (контакти, задачи, календари)." } }DAV-4-TbSync-1.8/_locales/de/000077500000000000000000000000001362346375200155215ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/de/dav.dtd000066400000000000000000000054611362346375200167760ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/de/dav.properties000066400000000000000000000143071362346375200204160ustar00rootroot00000000000000menu.name=CalDAV & CardDAV defaultname.calendar=Kalender defaultname.contacts=Kontakte syncstate.send.getfolders=Sende Anfrage bzgl. Ordnerliste syncstate.eval.folders=Verarbeite Ordnerliste syncstate.prepare.request.localchanges=Sende lokale Änderungen syncstate.send.request.localchanges=Warte auf Bestätigung der lokalen Änderungen syncstate.eval.response.localchanges=Verarbeite Bestätigung der lokalen Änderungen syncstate.send.request.remotechanges=Warte auf Daten vom Server syncstate.eval.response.remotechanges=Verarbeite Serverdaten status.networkerror=Verbindung zum Server fehlgeschlagen. status.404=HTTP Fehler 404 (angeforderte Resource nicht gefunden). status.403=Verbindung vom Server abgelehnt (nicht erlaubt). status.401=Authentifizierung fehlgeschlagen, überprüfen Sie den Benutzernamen und das Passwort. status.500=Unbekannter Server Fehler (HTTP Fehler 500). status.503=Service nicht erreichbar. status.softerror=Ignorierter Fehler (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=Wegen teilweise fehlender Schreibrechte wurden einige Aktionen vom Server zurückgewiesen und lokal rückgängig gemacht. status.malformed-xml=Antwort enthält fehlerhaftes XML, Sync abgebrochen. Prüfen Sie bitte das Ereignisprotokoll für weitere Details. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=Die automatische Erkennung der CalDAV & CardDAV Service-Endpunkte von „##replace.1##“ war nicht erfolgreich. Versuchen Sie es unter Angabe einer anderen Serveradresse erneut oder wechseln Sie zur benutzerdefinierten Konfiguration, um die Service-Endpunkte selbst anzugeben. status.rfc6764-lookup-failed=Die Abfrage von „##replace.1##“ lieferte nicht die benötigten Informationen bzgl. der CalDAV und CardDAV Service-Endpunkte. Bitte geben den Hostnamen ihres Servers an, um mit der automatischen Konfiguration fortzufahren. status.missing-permission=Fehlende Berechtigung: ##replace.1## status.caldavservernotfound=Es wurde kein CalDAV Server gefunden. status.carddavservernotfound=Es wurde kein CardDAV Server gefunden. config.custom=CalDAV & CardDAV Server Konfiguration add.serverprofile.discovery=Automatische Konfiguration add.serverprofile.discovery.description=Viele Dienstanbieter und Server unterstützen eine automatische Konfiguration, bei der nur eine E-Mail Adresse bzw. ein Benutzername und eine Serveradresse angegeben werden müssen. add.serverprofile.discovery.details1=Geben Sie für die automatische Erkennung der CalDAV- und CardDAV-Dienstendpunkte Ihre Zugangsdaten und den Hostnamen Ihres Servers ein (z.B. „cloud.myserver.de“). add.serverprofile.discovery.details2=Ist Ihr Benutzername eine E-Mail Adresse, wird die Angabe des Servers optional, da die Informationen bzgl. der Service-Endpunkte evtl. über eine RFC6764-Anfrage direkt von Ihrem Dienstanbieter bezogen werden können (falls dieser das unterstützt). add.serverprofile.discovery.server-optional=optionale Angabe add.serverprofile.custom=Benutzerdefinierte Konfiguration add.serverprofile.custom.description=Die CalDAV und CardDAV Service-Endpunkte können manuell konfiguriert werden. add.serverprofile.custom.details1=Die benötigten CalDAV & CardDAV Service-Endpunkte bzw. die Prinzipal-Adressen sollten Sie bei Ihrem Serviceanbieter erfragen können. add.serverprofile.custom.details2=Wenn Sie eine der beiden Adressen leer lassen, wird der entsprechende Dienst für dieses Konto deaktiviert. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux ist ein Dienst, der Kontakte, Kalender und Aufgaben synchronisiert. Sie wird von der Firma hinter sabre/dav angetrieben und hat ihren Sitz in Deutschland. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org ist ein sicherer, deutscher E-Mail-Anbieter für Privat- und Geschäftskunden, der auch Kalender, Kontakte und Cloud-Speicher bietet. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=Der angeforderte Benutzername ist Ihre Apple ID. Das angeforderte Passwort ist jedoch nicht das Passwort für Ihr Apple ID! Um mit TbSync auf Ihre Kontakte und Kalender zugreifen zu können, müssen Sie zwingend die Zwei-Faktor-Autorisierung für Ihr iCloud-Konto aktivieren und ein separates App-spezifisches Kennwort für TbSync erstellen. add.serverprofile.icloud.details2=Dies ist eine von Apple eingeführte zusätzliche Sicherheitsebene, sodass Drittanbieter-Clients keinen Zugriff auf Ihr Apple-Konto erhalten. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Europa) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (USA) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google verwendet eine moderne Authentifizierungsmethode und TbSync muss weder Ihren Benutzernamen noch Ihr Passwort kennen. Nachdem Sie auf "Weiter >" geklickt haben, wird sich ein Browserfenster öffnen, indem Sie sich an Ihrem Google-Konto anmelden können und dem "Provider for CalDAV & CardDAV" Zugriff auf Ihre Kontakte und Kalender erlauben können. acl.readwrite=Schreibrechte auf Server: ##replace.1## acl.readonly=Serverzugriff nur lesend (verwerfe lokale Änderungen) acl.modify=bearbeiten acl.add=hinzufügen acl.delete=löschen acl.none=keine add.spinner.validating=Überprüfe Verbindung zum Server add.spinner.query=Sende RFC6764-Anfrage an „##replace.1##“ autocomplete.WORK=dienstlich autocomplete.HOME=privat autocomplete.PREF=bevorzugt status.gContactSync=Es besteht eine Inkompatibilität mit gContactSync bei aktivierter Synchronisation der Kontaktgruppen. Bitte deaktivieren eines von beiden, solange der Fehler nicht behoben ist. DAV-4-TbSync-1.8/_locales/de/messages.json000066400000000000000000000003561362346375200202270ustar00rootroot00000000000000{ "extensionName": { "message": "Provider für CalDAV & CardDAV" }, "extensionDescription": { "message": "Erweitert TbSync und erlaubt die Synchronisation von CalDAV & CardDAV Konten (Kontakte, Aufgaben und Kalender)." } }DAV-4-TbSync-1.8/_locales/en-US/000077500000000000000000000000001362346375200160605ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/en-US/dav.dtd000066400000000000000000000052201362346375200173260ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/en-US/dav.properties000066400000000000000000000130711362346375200207520ustar00rootroot00000000000000menu.name=CalDAV & CardDAV defaultname.calendar=Calendar defaultname.contacts=Contacts syncstate.send.getfolders=Requesting folder list syncstate.eval.folders=Updating folder list syncstate.prepare.request.localchanges=Sending local changes syncstate.send.request.localchanges=Waiting for acknowledgment of local changes syncstate.eval.response.localchanges=Processing acknowledgment of local changes syncstate.send.request.remotechanges=Waiting for remote changes syncstate.eval.response.remotechanges=Processing remote changes status.networkerror=Could not connect to server. status.404=HTTP Error 404 (requested resource not found). status.403=Server rejected connection (forbidden). status.401=Could not authenticate, check username and password. status.500=Unknown Server Error (HTTP Error 500). status.503=Service unavailable. status.softerror=Non fatal error (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=Due to partially missing write permissions, some actions were rejected by the server and reversed locally. status.malformed-xml=Could not parse XML. Check event log for details. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=Automatic discovery of CalDAV & CardDAV service endpoints of “##replace.1##” has failed. Try again, specifying a different server address, or switch to the custom configuration to manually specify the service endpoints. status.rfc6764-lookup-failed=The query of “##replace.1##” did not provide the required information regarding the CalDAV and CardDAV service endpoints. Please enter the hostname of your server to proceed with the automatic configuration. status.missing-permission=Missing permission: ##replace.1## status.caldavservernotfound=Could not find a CalDAV server. status.carddavservernotfound=Could not find a CardDAV server. config.custom=CalDAV & CardDAV server configuration add.serverprofile.discovery=Automatic Configuration add.serverprofile.discovery.description=Many service providers and servers support an automatic configuration process, which requires only an e-mail address or a username and a server address. add.serverprofile.discovery.details1=For the automatic discovery of the CalDAV & CardDAV service endpoints, please enter your credentials and the host name of your server (e.g. "cloud.myserver.de"). add.serverprofile.discovery.details2=If your username is an e-mail address, specifying the server becomes optional, as the information about the service endpoints may be obtained directly from your service provider via an RFC6764 request (if supported). add.serverprofile.discovery.server-optional=optional add.serverprofile.custom=Manual Configuration add.serverprofile.custom.description=The CalDAV and CardDAV service endpoints can be configured manually. add.serverprofile.custom.details1=The required CalDAV & CardDAV service endpoints or the so called principal addresses should be provided by your service provider. add.serverprofile.custom.details2=If you leave either address empty, the corresponding service will be disabled for this account. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux is a service that syncs contacts, calendars and tasks. It's powered by the company behind sabre/dav and is based in Germany. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=The requested user name is your Apple ID. Please note, that you may not use your Apple ID password here. You MUST enable two-factor authentication (2FA) for your iCloud account and create a separate app-specific password for TbSync. add.serverprofile.icloud.details2=This is a security layer enforced by Apple, so that 3rd party clients do not gain access to your Apple account. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Europe) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (USA) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google uses a modern authentication method and TbSync does not need to know your username or your password. After clicking "Next >", a browser window will be opened, where you can log into your Google account and allow the "Provider for CalDAV & CardDAV" to access your contacts and calendars. acl.readwrite=Server write permissions: ##replace.1## acl.readonly=Read-only server access (revert local changes) acl.modify=modify acl.add=add acl.delete=delete acl.none=none add.spinner.validating=Verifying connection to server add.spinner.query=Sending RFC6764 request to “##replace.1##” autocomplete.WORK=business autocomplete.HOME=private autocomplete.PREF=preferred status.gContactSync=There is an incompatibility with gContactSync with activated contact group synchronization. Please deactivate one of them as long as the error is not resolved. DAV-4-TbSync-1.8/_locales/en-US/messages.json000066400000000000000000000002701362346375200205610ustar00rootroot00000000000000{ "extensionName": { "message": "Provider for CalDAV & CardDAV" }, "extensionDescription": { "message": "Add sync support for CalDAV & CardDAV accounts to TbSync." } } DAV-4-TbSync-1.8/_locales/fr/000077500000000000000000000000001362346375200155405ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/fr/dav.dtd000066400000000000000000000060701362346375200170120ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/fr/dav.properties000066400000000000000000000150771362346375200204420ustar00rootroot00000000000000menu.name=CalDAV & CardDAV defaultname.calendar=Agenda defaultname.contacts=Contacts syncstate.send.getfolders=Demande de la liste des dossiers syncstate.eval.folders=Mise à jour de la liste des dossiers syncstate.prepare.request.localchanges=Envoi des modifications locales syncstate.send.request.localchanges=En attente de la confirmation de réception des modifications locales syncstate.eval.response.localchanges=Traitement de la confirmation de réception des modifications locales syncstate.send.request.remotechanges=En attente des modifications distantes syncstate.eval.response.remotechanges=En cours de traitement des modifications distantes status.networkerror=La connexion au serveur a échoué. status.404=HTTP Erreur 404 (la ressource demandée n'a pas été trouvée). status.403=Le serveur a refusé la connexion (accès interdit). status.401=L'authentification a échoué. Veuillez vérifier le couple nom d'utilisateur/mot de passe. status.500=Erreur serveur inconnue(Erreur HTTP 500). status.503=Service indisponible. status.softerror=Erreur non critique (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=À cause de droits en écriture partiellement insuffisants, certaines actions ont été refusées par le serveur et annulées localement. status.malformed-xml=Analyse de l'XML impossible. Veuillez consulter le journal de débogage pour plus de détails. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/59#issuecomment-459685281 status.service-discovery-failed=La découverte automatique des URL d'accès aux services CalDAV et CardDAV de “##replace.1###” a échoué. Veuillez réessayer, en spécifiant une adresse de serveur différente, ou passez à la configuration personnalisée, afin de spécifier manuellement les URL en question. status.rfc6764-lookup-failed=L'appel à "##replace.1##” n'a pas permis de déterminer les informations nécessaires relatives aux URL des services CalDAV et CardDAV. Veuillez entrer le nom d'hôte de votre serveur afin de continuer la configuration automatique. status.missing-permission=Droit manquant: ##replace.1## status.caldavservernotfound=Impossible de trouver un serveur CalDAV. status.carddavservernotfound=Impossible de trouver un serveur CardDAV. config.custom=Configuration des serveurs CalDAV et CardDAV add.serverprofile.discovery=Configuration automatique add.serverprofile.discovery.description=De nombreux fournisseurs de services et de serveurs permettent un processus de configuration automatique, ce qui ne nécessite que l'adresse du serveur, ainsi qu'un nom d'utilisateur ou une adresse de courriel. add.serverprofile.discovery.details1=Pour la découverte automatique des URL d'accès aux services CalDAV & CardDAV, veuillez entrer vos identifiants et le nom d'hôte de votre serveur (par exemple "cloud.monserveur.example"). add.serverprofile.discovery.details2=Si votre nom d'utilisateur est une adresse e-mail, spécifier le serveur devient optionnel, car les informations sur les URL d'accès au service peuvent être obtenues automatiquement auprès de votre fournisseur de services via une requête RFC6764 (pour autant que votre fournisseur prenne en charge ce type de requêtes). add.serverprofile.discovery.server-optional=facultatif add.serverprofile.custom=Configuration manuelle add.serverprofile.custom.description=Les URL d'accès aux services CalDAV et CardDAV peuvent être configurées manuellement. add.serverprofile.custom.details1=Les URL d'accès aux services CalDAV et CardDAV ou ce qu'on appelle les "adresses principales" devraient vous avoir été fournies par votre fournisseur de services. add.serverprofile.custom.details2=Si vous laissez le champ d'adresse vide, le service correspondant sera désactivé pour ce compte. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux est un service de synchronisation de contacts, d'agendas et de tâches. Il est fourni par la société éditrice de sabre/dav et est basé en Allemagne. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org est un fournisseur de services de courriels allemand, à destination des particuliers et des entreprises. Ils fournissent également l'hébergement de calendriers, de contacts et des services cloud. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=Le nom d'utilisateur demandé est votre Apple ID. Veuillez noter que vous ne pouvez pas utiliser votre mot de passe Apple ID ici. Vous DEVEZ action l'authentification à deux facteurs (2FA) pour votre compte iCloud et créer un mot de passe d'application spécialement pour TbSync. add.serverprofile.icloud.details2=Il s'agit d'une mesure de sécurité imposée par Apple, afin que les clients tiers n'aient pas accès à l'ensemble de votre compte Apple. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Europe) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (USA) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google utilise une méthode d'authentification moderne, et Tbsync n'a pas besoin de connaître votre nom d'utilisateur ou votre mot de passe. Après avoir cliqué sur "Suivant", une fenêtre de navigateur s'ouvrira, dans laquelle vous pourrez vous connecter à votre compte Google et autoriser "Provider for CalDAV & CardDAV" à accéder à vos contacts et calendriers. acl.readwrite=Droits en écriture sur le serveur: ##replace.1## acl.readonly=Accès au serveur en lecture seule (annule les modification locales) acl.modify=modifier acl.add=ajouter acl.delete=supprimer acl.none=aucun add.spinner.validating=Vérification de la connexion au serveur add.spinner.query=Envoi d'une demande RFC6764 à «##replace.1## » autocomplete.WORK=travail autocomplete.HOME=privé autocomplete.PREF=favori status.gContactSync=Il y a une incompatibilité avec gContactSync, lorsque la synchronisation des groupes de contacts est activée. Tant que ce roblème n'est pas résolu, veuillez désactiver le compte ou la synchronisation des groupes. DAV-4-TbSync-1.8/_locales/fr/messages.json000066400000000000000000000003001362346375200202330ustar00rootroot00000000000000{ "extensionName": { "message": "Provider pour CalDAV & CardDAV" }, "extensionDescription": { "message": "Ajoute à TbSync la prise en charge des comptes CalDAV & CardDAV." } }DAV-4-TbSync-1.8/_locales/hu/000077500000000000000000000000001362346375200155455ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/hu/dav.dtd000066400000000000000000000057231362346375200170230ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/hu/dav.properties000066400000000000000000000137141362346375200204430ustar00rootroot00000000000000menu.name=CalDAV és CardDAV defaultname.calendar=naptár defaultname.contacts=kapcsolatok syncstate.send.getfolders=Mappalisták lekérése syncstate.eval.folders=Mappalisták frissítése syncstate.prepare.request.localchanges=Helyi változtatások küldése syncstate.send.request.localchanges=Várakozás a helyi változtatások igazolásra syncstate.eval.response.localchanges=Helyi változtatások igazolása feldolgozása syncstate.send.request.remotechanges=Várakozás a távoli változtatások syncstate.eval.response.remotechanges=A távoli változtatások feldolgozása status.networkerror=Nem tudott csatlakozni a kiszolgálóhoz. status.404=404-es HTTP hibakód (nem található). status.403=403-as HTTP hibakód (hozzáférés megtagadva/tiltott). status.401=Nem sikerült hitelesíteni, ellenőrizni a felhasználónevet és a jelszót. status.500=500-as hibakód (belső kiszolgálóhiba). status.503=503-as hibakód (a szolgáltatás nem érhető el). status.softerror=Kihagyta a hibát (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=A hiányzó írási jogosultságok miatt egyes műveleteket a szerver elutasított, és helyileg visszafordított. status.malformed-xml=Nem sikerült elemezni az XML-t. Ellenőrizze az eseménynaplót a részletekért. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=Automatic discovery of CalDAV & CardDAV service endpoints of “##replace.1##” has failed. Try again, specifying a different server address, or switch to the custom configuration to manually specify the service endpoints. status.rfc6764-lookup-failed=The query of “##replace.1##” did not provide the required information regarding the CalDAV and CardDAV service endpoints. Please enter the hostname of your server to proceed with the automatic configuration. status.missing-permission=Hiányzó engedély: ##replace.1## status.caldavservernotfound=Nem található CalDAV szerver. status.carddavservernotfound=Nem található CardDAV szerver. config.custom=A CalDAV és CardDAV kiszolgáló beállításai add.serverprofile.discovery=Önműködő konfigurálás add.serverprofile.discovery.description=Many service providers and servers support an automatic configuration process, which requires only an e-mail address or a username and a server address. add.serverprofile.discovery.details1=For the automatic discovery of the CalDAV & CardDAV service endpoints, please enter your credentials and the host name of your server (e.g. "cloud.myserver.de"). add.serverprofile.discovery.details2=If your username is an e-mail address, specifying the server becomes optional, as the information about the service endpoints may be obtained directly from your service provider via an RFC6764 request (if supported). add.serverprofile.discovery.server-optional=választható add.serverprofile.custom=Kézi beállítás add.serverprofile.custom.description=A CalDAV és a CardDAV szolgáltatás végpontjai kézzel beállításhatók. add.serverprofile.custom.details1=A szükséges CalDAV és CardDAV szolgáltatás végpontokat (principal addresses - főcímeket) a szolgáltatónak kell megadnia. add.serverprofile.custom.details2=Ha a két címet üresen hagyja, akkor a megfelelő szolgáltatás le lesz tiltva erre a fiókra. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=A „fruux” egy szolgáltatás, amely összehangolja a névjegyzékeket, naptárakat és feladatokat. A németország cég támogatottja a sabre/dav. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=A kért felhasználónév az Ön Apple-azonosítója. Vegye figyelembe, hogy itt nem használhatja az Apple-azonosító jelszavát. A 2FA (two-factor authentication – két faktoros hitelesítés) engedélyeznie kell iCloud-fiókja számára, és különálló alkalmazásspecifikus jelszót kell létrehoznia a Thunderbird-összehangolás számára. add.serverprofile.icloud.details2=Ez egy olyan biztonsági réteg, amelyet az Apple hajt végre, így harmadik fél ügyfelei nem férnek hozzá az Ön Apple-fiókjához. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Európa) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (Amerikai egyesült államok) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google uses a modern authentication method and TbSync does not need to know your username or your password. After clicking "Next >", a browser window will be opened, where you can log into your Google account and allow the "Provider for CalDAV & CardDAV" to access your contacts and calendars. acl.readwrite=írásjog: ##replace.1## acl.readonly=csak olvasható acl.modify=módosít acl.add=hozzáad acl.delete=töröl acl.none=egyik sem add.spinner.validating=Ellenőrizze a kapcsolatot a szerverrel add.spinner.query=RFC6764 kérés küldése a(z) „##replace.1##” címre autocomplete.WORK=üzlet autocomplete.HOME=magán autocomplete.PREF=előnyben részesített status.gContactSync=A gContactSync összeegyeztethetetlen az aktivált kapcsolattartó csoport összehangolásával. Kérjük, kapcsolja ki az egyiket, amíg a hiba nem oldódik meg. DAV-4-TbSync-1.8/_locales/hu/messages.json000066400000000000000000000004411362346375200202460ustar00rootroot00000000000000{ "extensionName": { "message": "A CalDAV és CardDAV szolgáltató" }, "extensionDescription": { "message": "A CalDAV- és CardDAV-fiókok összehangolás támogatásának hozzáadása a Thunderbird-összehangoláshoz (TbSync) (névjegyzék, feladatok és naptárak)." } }DAV-4-TbSync-1.8/_locales/it/000077500000000000000000000000001362346375200155455ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/it/dav.dtd000066400000000000000000000054711362346375200170230ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/it/dav.properties000066400000000000000000000136541362346375200204460ustar00rootroot00000000000000menu.name=CalDAV e CardDAV defaultname.calendar=calendario defaultname.contacts=contatti syncstate.send.getfolders=Richiesta elenco cartelle in corso syncstate.eval.folders=Aggiornamento elenco cartelle in corso syncstate.prepare.request.localchanges=Invio modifiche locali in corso syncstate.send.request.localchanges=In attesa del riconoscimento delle modifiche locali syncstate.eval.response.localchanges=Elaborazione riconoscimento modifiche locali in corso syncstate.send.request.remotechanges=In attesa delle modifiche remote syncstate.eval.response.remotechanges=Elaborazione modifiche remote in corso status.networkerror=Impossibile connettersi al server. status.404=Errore HTTP 404 (risorsa richiesta non trovata). status.403=Il server ha rifiutato la connessione (vietato). status.401=Impossibile autenticarsi, controllare nome utente e password. status.500=Errore server sconosciuto (errore HTTP 500). status.503=Servizio non disponibile. status.softerror=Errore saltato (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=A causa della mancanza di permessi di scrittura, alcune azioni sono state respinte dal server e annullate localmente. status.malformed-xml=Impossibile analizzare XML. Controllare il registro eventi per i dettagli. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=L'individuazione automatica degli endpoint servizio CalDAV e CardDAV di "##replace.1##" non è riuscita. Riprova specificando un indirizzo server diverso o passa alla modalità configurazione personalizzata per specificare gli endpoint servizio manualmente. status.rfc6764-lookup-failed=L'interrogazione di "##replace.1##" non ha fornito le informazioni richieste relative agli endpoint dei servizi CalDAV e CardDAV. Immettere il nome host del server per procedere con la configurazione automatica. status.missing-permission=Permesso mancante: ##replace.1## status.caldavservernotfound=Impossibile trovare un server CalDAV. status.carddavservernotfound=Impossibile trovare un server CardDAV. config.custom=Configurazione server CalDAV & CardDAV add.serverprofile.discovery=Configurazione automatica add.serverprofile.discovery.description=Molti provider di servizi e server supportano un processo di configurazione automatico che richiede solamente un indirizzo di posta elettronica o un nome utente e l'indirizzo del server. add.serverprofile.discovery.details1=Per rilevare automaticamente gli endpoint servizio CalDAV e CardDAV, immetti le tue credenziali e il nome host del server (ad es. "cloud.myserver.de"). add.serverprofile.discovery.details2=Se il tuo nome utente è un indirizzo di posta elettronica, specificare il server diventa facoltativo in quanto le informazioni sugli endpoint servizio possono essere ottenute direttamente dal provider di servizi tramite una richiesta RFC6764 (se supportata). add.serverprofile.discovery.server-optional=facoltativo add.serverprofile.custom=Configurazione personalizzata add.serverprofile.custom.description=Gli endpoint servizio CalDAV e CardDAV possono essere configurati manualmente. add.serverprofile.custom.details1=Gli endpoint servizio CalDAV e CardDAV richiesti o i cosiddetti indirizzi principali dovrebbero essere forniti dal tuo provider di servizi. add.serverprofile.custom.details2=Se si lascia un indirizzo vuoto, il servizio corrispondente sarà disabilitato per quest'account. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux è un servizio che sincronizza contatti, calendari e attività. È alimentato dalla società dietro sabre/dav e ha sede in Germania. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org è un provider di posta elettronica tedesco sicuro per i clienti privati e business che offre anche calendari, contatti e archiviazione cloud. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=Il nome utente richiesto è il tuo ID Apple. Nota che non puoi utilizzare la password del tuo ID Apple qui. DEVI abilitare l'autenticazione a due fattori per il tuo account iCloud e creare una password specifica dell'app per TbSync. add.serverprofile.icloud.details2=Questa è una misura di sicurezza imposta da Apple in modo che client di terze parti non ottengano l'accesso al tuo account Apple. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=La password richiesta è una password specifica per l'app per TbSync che puoi creare nel tuo portale web Yahoo!. Non è la tua password standard di Yahoo. add.serverprofile.gmx.net=GMX.net (Europa) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (Stati Uniti d'America) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google utilizza un metodo di autenticazione moderno e TbSync non ha bisogno di conoscere il tuo nome utente o la tua password. Dopo aver fatto clic su "Avanti >" verrà aperta una finestra del browser dove puoi accedere al tuo account Google e consentire al "Provider for CalDAV & CardDAV" di accedere ai tuoi contatti e calendari. acl.readwrite=Scrivi permesso: ##replace.1## acl.readonly=sola lettura acl.modify=modificare acl.add=inserisci acl.delete=elimina acl.none=nessuna add.spinner.validating=Controlla la connessione al server add.spinner.query=Invio richiesta RFC6764 a "##replace.1##" in corso autocomplete.WORK=lavoro autocomplete.HOME=personale autocomplete.PREF=preferito status.gContactSync=Il software è incompatibile con gContactSync con la sincronizzazione gruppi contatti attivata. Disattivarne uno finché l'errore non sarà risolto. DAV-4-TbSync-1.8/_locales/it/messages.json000066400000000000000000000003171362346375200202500ustar00rootroot00000000000000{ "extensionName": { "message": "Provider CalDAV e CardDAV" }, "extensionDescription": { "message": "Aggiunge il supporto per la sincronizzazione degli account CalDAV e CardDAV a TbSync." } }DAV-4-TbSync-1.8/_locales/pl/000077500000000000000000000000001362346375200155445ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/pl/dav.dtd000066400000000000000000000053601362346375200170170ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/pl/dav.properties000066400000000000000000000136241362346375200204420ustar00rootroot00000000000000menu.name=CalDAV i CardDAV defaultname.calendar=Kalendarz defaultname.contacts=Kontakty syncstate.send.getfolders=Żądanie listy folderów syncstate.eval.folders=Aktualizowanie listy folderów syncstate.prepare.request.localchanges=Wysyłanie zmian lokalnych syncstate.send.request.localchanges=Oczekiwanie na potwierdzenie lokalnych zmian syncstate.eval.response.localchanges=Przetwarzanie potwierdzenia lokalnych zmian syncstate.send.request.remotechanges=Oczekiwanie na zdalne zmiany syncstate.eval.response.remotechanges=Przetwarzanie zdalnych zmian status.networkerror=Nie można połączyć z serwerem. status.404=Błąd HTTP 404 (nie znaleziono żądanego zasobu). status.403=Serwer odrzucił połączenie (zabronione). status.401=Nie można uwierzytelnić, sprawdź nazwę użytkownika i hasło. status.500=Nieznany błąd serwera (błąd HTTP 500). status.503=Usługa niedostępna. status.softerror=Błąd niekrytyczny (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=Z powodu częściowo brakujących uprawnień do zapisu, niektóre działania zostały odrzucone przez serwer i cofnięte lokalnie. status.malformed-xml=Nie można przeanalizować XML. Sprawdź dziennik zdarzeń, aby uzyskać szczegółowe informacje. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=Automatyczne wykrywanie punktów końcowych usługi CalDAV i CardDAV dla “##replace.1##” nie powiodło się. Spróbuj ponownie, podając inny adres serwera lub przejdź do konfiguracji niestandardowej, aby ręcznie określić punkty końcowe usługi. status.rfc6764-lookup-failed=Zapytanie “##replace.1##” nie dostarczyło wymaganych informacji dotyczących punktów końcowych usługi CalDAV i CardDAV. Wprowadź nazwę hosta swojego serwera, aby kontynuować automatyczną konfigurację. status.missing-permission=Brak uprawnienia: ##replace.1## status.caldavservernotfound=Nie można znaleźć serwera CalDAV. status.carddavservernotfound=Nie można znaleźć serwera CardDAV. config.custom=Konfiguracja serwera CalDAV i CardDAV add.serverprofile.discovery=Automatyczna Konfiguracja add.serverprofile.discovery.description=Wielu dostawców usług i serwerów wspiera proces automatycznej konfiguracji, który wymaga tylko adresu e-mail lub nazwy użytkownika i adresu serwera. add.serverprofile.discovery.details1=Aby automatycznie wykryć punkty końcowe usługi CalDAV i CardDAV, wprowadź swoje dane uwierzytelniające i nazwę hosta swojego serwera (np. "cloud.myserver.de"). add.serverprofile.discovery.details2=Jeśli nazwa użytkownika to adres e-mail, określenie serwera staje się opcjonalne, ponieważ informacje o punktach końcowych usługi można uzyskać bezpośrednio od usługodawcy za pośrednictwem żądania RFC6764 (jeśli jest obsługiwane). add.serverprofile.discovery.server-optional=opcjonalnie add.serverprofile.custom=Ręczna Konfiguracja add.serverprofile.custom.description=Punkty końcowe usługi CalDAV i CardDAV można skonfigurować ręcznie. add.serverprofile.custom.details1=Wymagane punkty końcowe usługi CalDAV i CardDAV lub tak zwane adresy główne powinien podać dostawca usługi. add.serverprofile.custom.details2=Jeśli którykolwiek z adresów pozostanie pusty, odpowiednia usługa zostanie wyłączona dla tego konta. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux to usługa synchronizująca kontakty, kalendarze i zadania. Jest napędzana przez firmę opartą o sabre/dav i z siedzibą w Niemczech. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org to bezpieczny, niemiecki dostawca poczty elektronicznej dla klientów prywatnych i biznesowych, który oferuje również kalendarze, kontakty i przechowywanie w chmurze. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=Żądana nazwa użytkownika to Twój Apple ID. Pamiętaj, że nie możesz tutaj używać hasła Apple ID. Musisz włączyć uwierzytelnianie dwuskładnikowe (2FA) dla swojego konta iCloud i utworzyć osobne hasło aplikacji dla TbSync. add.serverprofile.icloud.details2=Jest to warstwa bezpieczeństwa wymuszona przez Apple, aby klienci zewnętrzni nie uzyskali dostępu do Twojego konta Apple. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=Żądane hasło jest hasłem specyficznym dla aplikacji dla TbSync, które możesz utworzyć w portalu Yahoo! To nie jest Twoje standardowe hasło Yahoo! add.serverprofile.gmx.net=GMX.net (Europe) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (USA) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google stosuje nowoczesną metodę uwierzytelniania, a TbSync nie musi znać Twojej nazwy użytkownika ani hasła. Po kliknięciu "Dalej >" otworzy się okno przeglądarki, w którym możesz zalogować się na swoje konto Google i zezwolić "Provider for CalDAV & CardDAV" na dostęp do Twoich kontaktów i kalendarzy. acl.readwrite=Uprawnienia do zapisu na serwerze: ##replace.1## acl.readonly=Dostęp do serwera tylko do odczytu (cofnij zmiany lokalne) acl.modify=zmień acl.add=dodaj acl.delete=usuń acl.none=żaden add.spinner.validating=Weryfikowanie połączenia z serwerem add.spinner.query=Wysyłanie żądania RFC6764 do “##replace.1##” autocomplete.WORK=służbowy autocomplete.HOME=domowy autocomplete.PREF=preferowane status.gContactSync=Występuje niezgodność z gContactSync z aktywowaną synchronizacją grupy kontaktów. Dezaktywuj jeden z nich, dopóki błąd nie zostanie rozwiązany. DAV-4-TbSync-1.8/_locales/pl/messages.json000066400000000000000000000002731362346375200202500ustar00rootroot00000000000000{ "extensionName": { "message": "Dostawca dla CalDAV i CardDAV" }, "extensionDescription": { "message": "Dodaj do TbSync wsparcie synchronizacji dla CalDAV i CardDAV." } }DAV-4-TbSync-1.8/_locales/pt_BR/000077500000000000000000000000001362346375200161375ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/pt_BR/dav.dtd000066400000000000000000000056351362346375200174170ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/pt_BR/dav.properties000066400000000000000000000140331362346375200210300ustar00rootroot00000000000000menu.name=CalDAV e CardDAV defaultname.calendar=calendário defaultname.contacts=contatos syncstate.send.getfolders=Solicitando lista de pastas syncstate.eval.folders=Atualizando lista de pastas syncstate.prepare.request.localchanges=Enviando alterações locais syncstate.send.request.localchanges=Aguardando confirmação de alterações locais syncstate.eval.response.localchanges=Processando reconhecimento de alterações locais syncstate.send.request.remotechanges=Esperando por alterações remotas syncstate.eval.response.remotechanges=Processando alterações remotas status.networkerror=Não foi possível conectar-se ao servidor. status.404=HTTP Erro 404 (recurso solicitado não encontrado). status.403=Conexão rejeitada pelo servidor (proibida). status.401=Não foi possível autenticar, verifique o nome de usuário e a senha. status.500=Erro de servidor desconhecido (HTTP Erro 500). status.503=Serviço indisponível. status.softerror=Erro não fatal (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=Devido à falta parcial de permissões de gravação, algumas ações foram rejeitadas pelo servidor e revertidas localmente. status.malformed-xml=Não foi possível analisar XML. Verifique o log de eventos para obter detalhes. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=A descoberta automática dos serviços de CalDAV & CardDAV de "##replace.1##" falhou. Tente novamente, especificando um endereço de servidor diferente ou altere para a configuração personalizada para especificar manualmente as configurações. status.rfc6764-lookup-failed=A consulta de "##replace.1##" não forneceu as informações necessárias sobre os serviços CalDAV e CardDAV. Por favor, digite o nome do host do seu servidor para prosseguir com a configuração automática. status.missing-permission=Permissão ausente: ##replace.1## status.caldavservernotfound=Não foi possível encontrar um servidor CalDAV. status.carddavservernotfound=Não foi possível encontrar um servidor CardDAV. config.custom=Configurações do servidor CalDAV & CardDAV add.serverprofile.discovery=Configuração Automática add.serverprofile.discovery.description=Muitos provedores de serviço e servidores suportam um processo de configuração automática, que requer apenas um endereço de e-mail ou um nome de usuário e um endereço de servidor. add.serverprofile.discovery.details1=Para a descoberta automática dos pontos de extremidade do serviço CalDAV & CardDAV, por favor insira suas credenciais e o nome do host do seu servidor (ex.: "cloud.myserver.de"). add.serverprofile.discovery.details2=Se seu nome de usuário é um endereço de e-mail, especificar o servidor torna-se opcional, como as informações sobre os pontos de extremidade do serviço podem ser obtidas diretamente do seu provedor de serviços através de um pedido RFC6764 (se suportado). add.serverprofile.discovery.server-optional=opcional add.serverprofile.custom=Configuração manual add.serverprofile.custom.description=Os serviços de CalDAV e CardDAV podem ser configurados manualmente. add.serverprofile.custom.details1=Os serviços de CalDAV e CardDAV necessários ou endereços principais devem ser fornecidos pelo seu provedor de serviços. add.serverprofile.custom.details2=Se você deixar o endereço vazio, o serviço correspondente será desativado para essa conta. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux é um serviço que sincroniza contatos, calendários e tarefas. É alimentado pela empresa por trás de saber/dav e é baseado na Alemanha. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org é um provedor de e-mail alemão seguro para uso pessoal e para negócios, que também oferece calendários, contatos e armazenamento na nuvem. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=O nome do usuário solicitado é seu ID da Apple. Por favor, note que você não pode usar sua senha da Apple ID aqui. Você DEVE ativar a autenticação de dois fatores (2FA) para sua conta do iCloud e criar uma senha específica do aplicativo separada para o TbSync. add.serverprofile.icloud.details2=Essa é uma camada de segurança imposta pela Apple, portanto, os clientes de terceiros não obtêm acesso à sua conta da Apple. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Europa) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (E.U.A.) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=O Google usa um método moderno de autenticação e o TbSync não precisa saber seu nome de usuário nem sua senha. Depois de clicar em "Avançar>", uma janela do navegador será aberta, onde você poderá fazer login na sua conta do Google e permitir que o "Provider for CalDAV & CardDAV" acesse seus contatos e calendários. acl.readwrite=Permissões de gravação do servidor: ##replace.1## acl.readonly=Acesso ao servidor somente leitura (reverter alterações locais) acl.modify=modificar acl.add=adicionar acl.delete=excluir acl.none=nenhum add.spinner.validating=Verifique a conexão com o servidor add.spinner.query=Enviando pedido RFC6764 para "##replace.1##" autocomplete.WORK=empresa autocomplete.HOME=particular autocomplete.PREF=preferido status.gContactSync=Existe uma incompatibilidade com o gContactSync com a sincronização de grupo de contatos ativado. Desative um deles, desde até que o erro não seja resolvido. DAV-4-TbSync-1.8/_locales/pt_BR/messages.json000066400000000000000000000003011362346375200206330ustar00rootroot00000000000000{ "extensionName": { "message": "Provedor CalDAV e CardDAV" }, "extensionDescription": { "message": "Adiciona suporte para sincronizar contas CalDAV e CardDAV com o TbSync." } }DAV-4-TbSync-1.8/_locales/ru/000077500000000000000000000000001362346375200155575ustar00rootroot00000000000000DAV-4-TbSync-1.8/_locales/ru/dav.dtd000066400000000000000000000075041362346375200170340ustar00rootroot00000000000000 DAV-4-TbSync-1.8/_locales/ru/dav.properties000066400000000000000000000160111362346375200204460ustar00rootroot00000000000000menu.name=CalDAV & CardDAV defaultname.calendar=календарь defaultname.contacts=контакты syncstate.send.getfolders=Запрос списка папок syncstate.eval.folders=Обновление списка папок syncstate.prepare.request.localchanges=Отправка локальных изменений syncstate.send.request.localchanges=Ожидание подтверждения локальных изменений syncstate.eval.response.localchanges=Обработка подтверждения локальных изменений syncstate.send.request.remotechanges=Ожидание удаленных изменений syncstate.eval.response.remotechanges=Обработка удаленных изменений status.networkerror=Не удалось подключиться к серверу. status.404=Запрошенный ресурс не найден (HTTP Ошибка 404). status.403=Сервер отклонил соединение (запрещено) (HTTP Ошибка 403). status.401=Не удалось аутентифицировать, проверить имя пользователя и пароль. (HTTP Ошибка 401). status.500=Неизвестная ошибка сервера (HTTP Ошибка 500). status.503=Сервис недоступен (HTTP Ошибка 503). status.softerror=пропущенная ошибка (##replace.1##) status.success.managed-by-lightning=Lightning status.info.restored=Из-за частично отсутствующих разрешений на запись некоторые действия были отклонены сервером и отменены локально. status.malformed-xml=Не удалось разобрать XML. Проверьте журнал событий для деталей. helplink.malformed-xml=https://github.com/jobisoft/DAV-4-TbSync/issues/104 status.service-discovery-failed=Automatic discovery of CalDAV & CardDAV service endpoints of “##replace.1##” has failed. Try again, specifying a different server address, or switch to the custom configuration to manually specify the service endpoints. status.rfc6764-lookup-failed=The query of “##replace.1##” did not provide the required information regarding the CalDAV and CardDAV service endpoints. Please enter the hostname of your server to proceed with the automatic configuration. status.missing-permission=Отсутствует разрешение: ##replace.1## status.caldavservernotfound=Не удалось найти сервер CalDAV. status.carddavservernotfound=Не удалось найти сервер CardDAV. config.custom=CalDAV & CardDAV конфигурация сервера add.serverprofile.discovery=Автоматическая настройка add.serverprofile.discovery.description=Many service providers and servers support an automatic configuration process, which requires only an e-mail address or a username and a server address. add.serverprofile.discovery.details1=For the automatic discovery of the CalDAV & CardDAV service endpoints, please enter your credentials and the host name of your server (e.g. "cloud.myserver.de"). add.serverprofile.discovery.details2=If your username is an e-mail address, specifying the server becomes optional, as the information about the service endpoints may be obtained directly from your service provider via an RFC6764 request (if supported). add.serverprofile.discovery.server-optional=optional add.serverprofile.custom=Пользовательская конфигурация add.serverprofile.custom.description=The CalDAV and CardDAV service endpoints can be configured manually. add.serverprofile.custom.details1=The required CalDAV & CardDAV service endpoints or the so called principal addresses should be provided by your service provider. add.serverprofile.custom.details2=Если вы оставите любой адрес пустым, соответствующая служба будет отключена для этого аккаунта. add.serverprofile.fruux=fruux add.serverprofile.fruux.description=fruux - это сервис, который синхронизирует контакты, календари и задачи. Он работает на базе компании поддерживающей протокол sabre/dav и базируется в Германии. add.serverprofile.posteo=Posteo add.serverprofile.posteo.description=https://www.posteo.de add.serverprofile.mbo=mailbox.org add.serverprofile.mbo.description=mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage. add.serverprofile.icloud=iCloud add.serverprofile.icloud.description=https://www.icloud.com add.serverprofile.icloud.details1=Запрашиваемое имя пользователя - ваш Apple-ID. Обратите внимание, что вы не можете использовать свой пароль Apple-ID здесь. Вы ДОЛЖНЫ включить двухфакторную авторизацию для своей учетной записи iCloud и создать отдельный пароль приложения для TbSync. add.serverprofile.icloud.details2=Это уровень безопасности, применяемый Apple, поэтому сторонние клиенты не могут получить доступ к вашей учетной записи Apple. add.serverprofile.yahoo=Yahoo! add.serverprofile.yahoo.description=https://www.yahoo.com add.serverprofile.yahoo.details1=The requested password is an app-specific password for TbSync which you can create in your Yahoo! web portal. It is not your standard Yahoo! password. add.serverprofile.gmx.net=GMX.net (Европа) add.serverprofile.gmx.net.description=https://www.gmx.net add.serverprofile.gmx.com=GMX.com (США) add.serverprofile.gmx.com.description=https://www.gmx.com add.serverprofile.web.de=WEB.de add.serverprofile.web.de.description=https://www.web.de add.serverprofile.google=Google add.serverprofile.google.description=https://accounts.google.com add.serverprofile.google.details1=Google uses a modern authentication method and TbSync does not need to know your username or your password. After clicking "Next >", a browser window will be opened, where you can log into your Google account and allow the "Provider for CalDAV & CardDAV" to access your contacts and calendars. acl.readwrite=Разрешение на запись: ##replace.1## acl.readonly=только для чтения acl.modify=модифицировать acl.add=добавлять acl.delete=удалять acl.none=никто add.spinner.validating=Проверьте соединение с сервером add.spinner.query=Sending RFC6764 request to “##replace.1##” autocomplete.WORK=business autocomplete.HOME=private autocomplete.PREF=preferred status.gContactSync=There is an incompatibility with gContactSync with activated contact group synchronization. Please deactivate one of them as long as the error is not resolved. DAV-4-TbSync-1.8/_locales/ru/messages.json000066400000000000000000000005641362346375200202660ustar00rootroot00000000000000{ "extensionName": { "message": "Provider for CalDAV & CardDAV" }, "extensionDescription": { "message": "Добавляет в TbSync поддержку базирующегося на http/https протокола синхронизации для учетных записей CalDAV & CardDAV (контакты, задачи и календари)." } }DAV-4-TbSync-1.8/beta-release-channel-update.json000066400000000000000000000005031362346375200214600ustar00rootroot00000000000000{ "addons": { "dav4tbsync@jobisoft.de": { "updates": [ { "version": "%VERSION%", "update_info_url": "https://github.com/jobisoft/DAV-4-TbSync/releases", "update_link": "%LINK%", "applications": { "gecko": { "strict_min_version": "68.0" } } } ] } } }DAV-4-TbSync-1.8/bootstrap.js000066400000000000000000000063311362346375200157260ustar00rootroot00000000000000/* * This file is part of DAV-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // no need to create namespace, we are in a sandbox var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); let thisID = ""; let component = {}; let onInitDoneObserver = { observe: async function (aSubject, aTopic, aData) { let valid = false; try { var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); valid = TbSync.enabled; } catch (e) { // If this fails, TbSync is not loaded yet and we will get the notification later again. } //load this provider add-on into TbSync if (valid) { await TbSync.providers.loadProvider(thisID, "dav", "chrome://dav4tbsync/content/provider.js"); } } } function install(data, reason) { } function uninstall(data, reason) { } function startup(data, reason) { // Possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE. thisID = data.id; Services.obs.addObserver(onInitDoneObserver, "tbsync.observer.initialized", false); Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/tbSyncDavCalendar.js", component); let registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); registrar.registerFactory( component.tbSyncDavCalendar.prototype.classID, component.tbSyncDavCalendar.prototype.classDescription, component.tbSyncDavCalendar.prototype.contractID, component.NSGetFactory(component.tbSyncDavCalendar.prototype.classID) ); // The startup of TbSync is delayed until all add-ons have called their startup(), // so all providers have registered the "tbsync.observer.initialized" observer. // Once TbSync has finished its startup, all providers will be notified (also if // TbSync itself is restarted) to load themself. // If this is not startup, we need load manually. if (reason != APP_STARTUP) { onInitDoneObserver.observe(); } } function shutdown(data, reason) { // Possible reasons: APP_SHUTDOWN, ADDON_DISABLE, ADDON_UNINSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE. // When the application is shutting down we normally don't have to clean up. if (reason == APP_SHUTDOWN) { return; } let registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar); registrar.unregisterFactory( component.tbSyncDavCalendar.prototype.classID, component.NSGetFactory(component.tbSyncDavCalendar.prototype.classID) ); Services.obs.removeObserver(onInitDoneObserver, "tbsync.observer.initialized"); //unload this provider add-on from TbSync try { var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); TbSync.providers.unloadProvider("dav"); } catch (e) { //if this fails, TbSync has been unloaded already and has unloaded this addon as well } Services.obs.notifyObservers(null, "chrome-flush-caches", null); } DAV-4-TbSync-1.8/chrome.manifest000066400000000000000000000006751362346375200163650ustar00rootroot00000000000000content dav4tbsync content/ skin dav4tbsync classic/1.0 skin/ locale dav4tbsync bg _locales/bg/ locale dav4tbsync de _locales/de/ locale dav4tbsync en-US _locales/en-US/ locale dav4tbsync fr _locales/fr/ locale dav4tbsync hu _locales/hu/ locale dav4tbsync it _locales/it/ locale dav4tbsync pl _locales/pl/ locale dav4tbsync pt-BR _locales/pt_BR/ locale dav4tbsync ru _locales/ru/ DAV-4-TbSync-1.8/content/000077500000000000000000000000001362346375200150225ustar00rootroot00000000000000DAV-4-TbSync-1.8/content/includes/000077500000000000000000000000001362346375200166305ustar00rootroot00000000000000DAV-4-TbSync-1.8/content/includes/abUI.js000066400000000000000000000517271362346375200200220ustar00rootroot00000000000000/* * This file is part of DAV-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var ui = { getUriFromDirectoryId: function(ownerId) { let directories = MailServices.ab.directories; while (directories.hasMoreElements()) { let directory = directories.getNext(); if (directory instanceof Components.interfaces.nsIAbDirectory) { if (ownerId.startsWith(directory.dirPrefId)) return directory.URI; } } return null; }, //function to get correct uri of current card for global book as well for mailLists getSelectedUri : function(aUri, aCard) { if (aUri == "moz-abdirectory://?") { //get parent via card owner let ownerId = aCard.directoryId; return dav.ui.getUriFromDirectoryId(ownerId); } else if (MailServices.ab.getDirectory(aUri).isMailList) { //MailList suck, we have to cut the url to get the parent return aUri.substring(0, aUri.lastIndexOf("/")) } else { return aUri; } }, //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //* Functions to handle advanced UI elements of AB //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * updatePref: function(aDocument, icon, toggle = false) { if (toggle) { if (icon.parentNode.meta.includes("PREF")) icon.parentNode.meta = icon.parentNode.meta.filter(e => e != "PREF"); else icon.parentNode.meta.push("PREF"); icon.parentNode.updateFunction (aDocument); } if (icon.parentNode.meta.includes("PREF")) { icon.setAttribute("src", "chrome://dav4tbsync/skin/type.pref.png"); } else { icon.setAttribute("src", "chrome://dav4tbsync/skin/type.nopref.png"); } }, updateType: function(aDocument, button, newvalue = null) { if (newvalue) { //we declare allowedValues to be non-overlapping -> remove all allowed values and just add the newvalue button.parentNode.meta = button.parentNode.meta.filter(value => -1 == button.allowedValues.indexOf(value)); if (button.allowedValues.includes(newvalue)) { //hardcoded sort order: HOME/WORK always before other types if (["HOME","WORK"].includes(newvalue)) button.parentNode.meta.unshift(newvalue); else button.parentNode.meta.push(newvalue); } button.parentNode.updateFunction (aDocument); } let intersection = button.parentNode.meta.filter(value => -1 !== button.allowedValues.indexOf(value)); let buttonType = (intersection.length > 0) ? intersection[0].toLowerCase() : button.otherIcon; button.setAttribute("image","chrome://dav4tbsync/skin/type."+buttonType+"10.png"); }, dragdrop: { handleEvent(event) { //only allow to drag the elements which are valid drag targets if (event.target.getAttribute("dragtarget") != "true") { event.stopPropagation(); return; } let outerbox = event.currentTarget; let richlistitem = outerbox.parentNode; switch (event.type) { case "dragenter": case "dragover": let dropIndex = richlistitem.parentNode.getIndexOfItem(richlistitem); let dragIndex = richlistitem.parentNode.getIndexOfItem(richlistitem.ownerDocument.getElementById(event.dataTransfer.getData("id"))); let centerY = event.currentTarget.clientHeight / 2; let insertBefore = (event.offsetY < centerY); let moveNeeded = !(dropIndex == dragIndex || (dropIndex+1 == dragIndex && !insertBefore) || (dropIndex-1 == dragIndex && insertBefore)); if (moveNeeded) { if (insertBefore) { richlistitem.parentNode.insertBefore(richlistitem.parentNode.getItemAtIndex(dragIndex), richlistitem); } else { richlistitem.parentNode.insertBefore(richlistitem.parentNode.getItemAtIndex(dragIndex), richlistitem.nextSibling); } } event.preventDefault(); break; case "drop": event.preventDefault(); case "dragleave": break; case "dragstart": event.currentTarget.style["background-color"] = "#eeeeee"; event.dataTransfer.setData("id", richlistitem.id); break; case "dragend": event.currentTarget.style["background-color"] = "transparent"; outerbox.updateFunction(outerbox.ownerDocument); break; default: return undefined; } }, }, //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //* Functions to handle multiple email addresses in AB (UI) //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * getNewEmailDetailsRow: function (aWindow, aItemData) { let emailType = "other"; if (aItemData.meta.includes("HOME")) emailType = "home"; else if (aItemData.meta.includes("WORK")) emailType = "work"; //first column let vbox = aWindow.document.createXULElement("vbox"); vbox.setAttribute("class","CardViewText"); vbox.setAttribute("style","margin-right:1ex; margin-bottom:2px;"); let image = aWindow.document.createXULElement("image"); image.setAttribute("width","10"); image.setAttribute("height","10"); image.setAttribute("src", "chrome://dav4tbsync/skin/type."+emailType+"10.png"); vbox.appendChild(image); //second column let description = aWindow.document.createXULElement("description"); description.setAttribute("class","plain"); let namespace = aWindow.document.lookupNamespaceURI("html"); let a = aWindow.document.createElementNS(namespace, "a"); a.setAttribute("href", "mailto:" + aItemData.value); a.textContent = aItemData.value; description.appendChild(a); if (aItemData.meta.includes("PREF")) { let pref = aWindow.document.createXULElement("image"); pref.setAttribute("style", "margin-left:1ex;"); pref.setAttribute("width", "11"); pref.setAttribute("height", "10"); pref.setAttribute("src", "chrome://dav4tbsync/skin/type.nopref.png"); description.appendChild(pref); } //row let row = aWindow.document.createXULElement("row"); row.setAttribute("align","end"); row.appendChild(vbox); row.appendChild(description); return row; }, getNewEmailListItem: function (aDocument, aItemData) { //hbox let outerhbox = aDocument.createXULElement("hbox"); outerhbox.setAttribute("dragtarget", "true"); outerhbox.setAttribute("flex", "1"); outerhbox.setAttribute("align", "center"); outerhbox.updateFunction = dav.ui.updateEmails; outerhbox.meta = aItemData.meta; outerhbox.addEventListener("dragenter", dav.ui.dragdrop); outerhbox.addEventListener("dragover", dav.ui.dragdrop); outerhbox.addEventListener("dragleave", dav.ui.dragdrop); outerhbox.addEventListener("dragstart", dav.ui.dragdrop); outerhbox.addEventListener("dragend", dav.ui.dragdrop); outerhbox.addEventListener("drop", dav.ui.dragdrop); outerhbox.style["background-image"] = "url('chrome://dav4tbsync/skin/dragdrop.png')"; outerhbox.style["background-position"] = "right"; outerhbox.style["background-repeat"] = "no-repeat"; //button let button = aDocument.createXULElement("button"); button.allowedValues = ["HOME", "WORK"]; button.otherIcon = "other"; button.setAttribute("type", "menu"); button.setAttribute("class", "plain"); button.setAttribute("style", "width: 35px; min-width: 35px; margin: 0;"); button.appendChild(aDocument.getElementById("DavEmailSpacer").children[0].cloneNode(true)); outerhbox.appendChild(button); //email box let emailbox = aDocument.createXULElement("hbox"); emailbox.setAttribute("flex", "1"); emailbox.setAttribute("style", "padding-bottom:1px"); let email = aDocument.createXULElement("textbox"); email.setAttribute("flex", "1"); email.setAttribute("class", "plain"); email.setAttribute("value", aItemData.value); email.addEventListener("change", function(e) {dav.ui.updateEmails(aDocument)}); email.addEventListener("keydown", function(e) {if (e.key == "Enter") {e.stopPropagation(); e.preventDefault(); if (e.target.value != "") { dav.ui.addEmailEntry(e.target.ownerDocument); }}}); emailbox.appendChild(email); outerhbox.appendChild(emailbox); //image let image = aDocument.createXULElement("image"); image.setAttribute("width", "11"); image.setAttribute("height", "10"); image.setAttribute("style", "margin:2px 20px 2px 1ex"); image.addEventListener("click", function(e) { dav.ui.updatePref(aDocument, e.target, true); }); outerhbox.appendChild(image); //richlistitem let richlistitem = aDocument.createXULElement("richlistitem"); richlistitem.setAttribute("id", "entry_" + TbSync.generateUUID()); richlistitem.appendChild(outerhbox); return richlistitem; }, getEmailListItemElement: function(item, element) { switch (element) { case "dataContainer": return item.children[0]; case "button": return item.children[0].children[0]; case "email": return item.children[0].children[1].children[0]; case "pref": return item.children[0].children[2]; default: return null; } }, addEmailEntry: function(aDocument) { let list = aDocument.getElementById("X-DAV-EmailAddressList"); let data = {value: "", meta: ["HOME"]}; let item = list.appendChild(dav.ui.getNewEmailListItem(aDocument, data)); list.ensureElementIsVisible(item); dav.ui.updateType(aDocument, dav.ui.getEmailListItemElement(item, "button")); dav.ui.updatePref(aDocument, dav.ui.getEmailListItemElement(item, "pref")); dav.ui.getEmailListItemElement(item, "email").focus(); }, //if any setting changed, we need to update Primary and Secondary Email Fields updateEmails: function(aDocument) { let list = aDocument.getElementById("X-DAV-EmailAddressList"); let emails = []; for (let i=0; i < list.children.length; i++) { let item = list.children[i]; let email = dav.ui.getEmailListItemElement(item, "email").value.trim(); if (email != "") { let json = {}; json.meta = dav.ui.getEmailListItemElement(item, "dataContainer").meta; json.value = email; emails.push(json); } } aDocument.getElementById("X-DAV-JSON-Emails").value = JSON.stringify(emails); //now update all other TB email fields based on the new JSON data let emailData = dav.tools.getEmailsFromJSON(aDocument.getElementById("X-DAV-JSON-Emails").value); for (let field in emailData) { if (emailData.hasOwnProperty(field)) { aDocument.getElementById(field).value = emailData[field].join(", "); } } }, //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * //* Functions to handle multiple phone numbers in AB (UI) //* * * * * * * * * * * * * * * * * * * * * * * * * * * * * getNewPhoneDetailsRow: function (aWindow, aItemData) { let phoneType1 = ""; if (aItemData.meta.includes("HOME")) phoneType1 = "home"; else if (aItemData.meta.includes("WORK")) phoneType1 = "work"; let phoneType2 = ""; if (aItemData.meta.includes("CELL")) phoneType2 = "cell"; else if (aItemData.meta.includes("FAX")) phoneType2 = "fax"; else if (aItemData.meta.includes("PAGER")) phoneType2 = "pager"; else if (aItemData.meta.includes("CAR")) phoneType2 = "car"; else if (aItemData.meta.includes("VIDEO")) phoneType2 = "video"; else if (aItemData.meta.includes("VOICE")) phoneType2 = "voice"; //first column let vbox = aWindow.document.createXULElement("hbox"); vbox.setAttribute("pack","end"); vbox.setAttribute("class","CardViewText"); vbox.setAttribute("style","margin-bottom:3px;"); if (phoneType1) { let image = aWindow.document.createXULElement("image"); image.setAttribute("style","margin-right:1ex;"); image.setAttribute("width","10"); image.setAttribute("height","10"); image.setAttribute("src", "chrome://dav4tbsync/skin/type."+phoneType1+"10.png"); vbox.appendChild(image); } if (phoneType2) { let image = aWindow.document.createXULElement("image"); image.setAttribute("style","margin-right:1ex;"); image.setAttribute("width","10"); image.setAttribute("height","10"); image.setAttribute("src", "chrome://dav4tbsync/skin/type."+phoneType2+"10.png"); vbox.appendChild(image); } //second column let description = aWindow.document.createXULElement("description"); description.setAttribute("class","plain"); description.setAttribute("style","-moz-user-select: text;"); description.textContent = aItemData.value; if (aItemData.meta.includes("PREF")) { let pref = aWindow.document.createXULElement("image"); pref.setAttribute("style", "margin-left:1ex;"); pref.setAttribute("width", "11"); pref.setAttribute("height", "10"); pref.setAttribute("src", "chrome://dav4tbsync/skin/type.nopref.png"); description.appendChild(pref); } //row let row = aWindow.document.createXULElement("row"); row.setAttribute("align","end"); row.appendChild(vbox); row.appendChild(description); return row; }, getNewPhoneListItem: function (aDocument, aItemData) { //hbox let outerhbox = aDocument.createXULElement("hbox"); outerhbox.setAttribute("dragtarget", "true"); outerhbox.setAttribute("flex", "1"); outerhbox.setAttribute("align", "center"); outerhbox.updateFunction = dav.ui.updatePhoneNumbers; outerhbox.meta = aItemData.meta; outerhbox.addEventListener("dragenter", dav.ui.dragdrop); outerhbox.addEventListener("dragover", dav.ui.dragdrop); outerhbox.addEventListener("dragleave", dav.ui.dragdrop); outerhbox.addEventListener("dragstart", dav.ui.dragdrop); outerhbox.addEventListener("dragend", dav.ui.dragdrop); outerhbox.addEventListener("drop", dav.ui.dragdrop); outerhbox.style["background-image"] = "url('chrome://dav4tbsync/skin/dragdrop.png')"; outerhbox.style["background-position"] = "right"; outerhbox.style["background-repeat"] = "no-repeat"; //button1 let button1 = aDocument.createXULElement("button"); button1.allowedValues = ["HOME", "WORK"]; button1.otherIcon = "none"; button1.setAttribute("type", "menu"); button1.setAttribute("class", "plain"); button1.setAttribute("style", "width: 35px; min-width: 35px; margin: 0;"); button1.appendChild(aDocument.getElementById("DavEmailSpacer").children[1].cloneNode(true)); outerhbox.appendChild(button1); //button2 let button2 = aDocument.createXULElement("button"); button2.allowedValues = ["CELL", "FAX", "PAGER", "CAR", "VIDEO", "VOICE"] ; //same order as in getNewPhoneDetailsRow button2.otherIcon = "none"; button2.setAttribute("type", "menu"); button2.setAttribute("class", "plain"); button2.setAttribute("style", "width: 35px; min-width: 35px; margin: 0;"); button2.appendChild(aDocument.getElementById("DavEmailSpacer").children[2].cloneNode(true)); outerhbox.appendChild(button2); //phone box let phonebox = aDocument.createXULElement("hbox"); phonebox.setAttribute("flex", "1"); phonebox.setAttribute("style", "padding-bottom:1px"); let phone = aDocument.createXULElement("textbox"); phone.setAttribute("flex", "1"); phone.setAttribute("class", "plain"); phone.setAttribute("value", aItemData.value); phone.addEventListener("change", function(e) {dav.ui.updatePhoneNumbers(aDocument)}); phone.addEventListener("keydown", function(e) {if (e.key == "Enter") {e.stopPropagation(); e.preventDefault(); if (e.target.value != "") { dav.ui.addPhoneEntry(e.target.ownerDocument); }}}); phonebox.appendChild(phone); outerhbox.appendChild(phonebox); //image let image = aDocument.createXULElement("image"); image.setAttribute("width", "11"); image.setAttribute("height", "10"); image.setAttribute("style", "margin:2px 20px 2px 1ex"); image.addEventListener("click", function(e) { dav.ui.updatePref(aDocument, e.target, true); }); outerhbox.appendChild(image); //richlistitem let richlistitem = aDocument.createXULElement("richlistitem"); richlistitem.setAttribute("id", "entry_" + TbSync.generateUUID()); richlistitem.appendChild(outerhbox); return richlistitem; }, updatePhoneNumbers: function(aDocument) { let list = aDocument.getElementById("X-DAV-PhoneNumberList"); let phones = []; for (let i=0; i < list.children.length; i++) { let item = list.children[i]; let phone = dav.ui.getPhoneListItemElement(item, "phone").value.trim(); if (phone != "") { let json = {}; json.meta = dav.ui.getPhoneListItemElement(item, "dataContainer").meta; json.value = phone; phones.push(json); } } aDocument.getElementById("X-DAV-JSON-Phones").value = JSON.stringify(phones); //now update all other TB number fields based on the new JSON data let phoneData = dav.tools.getPhoneNumbersFromJSON(aDocument.getElementById("X-DAV-JSON-Phones").value); for (let field in phoneData) { if (phoneData.hasOwnProperty(field)) { aDocument.getElementById(field).value = phoneData[field].join(", "); } } }, addPhoneEntry: function(aDocument) { let list = aDocument.getElementById("X-DAV-PhoneNumberList"); let data = {value: "", meta: ["VOICE"]}; let item = list.appendChild(dav.ui.getNewPhoneListItem(aDocument, data)); list.ensureElementIsVisible(item); dav.ui.updateType(aDocument, dav.ui.getPhoneListItemElement(item, "button1")); dav.ui.updateType(aDocument, dav.ui.getPhoneListItemElement(item, "button2")); dav.ui.updatePref(aDocument, dav.ui.getPhoneListItemElement(item, "pref")); dav.ui.getPhoneListItemElement(item, "phone").focus(); }, getPhoneListItemElement: function(item, element) { switch (element) { case "dataContainer": return item.children[0]; case "button1": return item.children[0].children[0]; case "button2": return item.children[0].children[1]; case "phone": return item.children[0].children[2].children[0]; case "pref": return item.children[0].children[3]; default: return null; } }, } DAV-4-TbSync-1.8/content/includes/network.js000066400000000000000000000621221362346375200206620ustar00rootroot00000000000000/* * This file is part of DAV-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var { HttpRequest } = ChromeUtils.import("chrome://tbsync/content/HttpRequest.jsm"); var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm"); const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm"); var network = { getAuthData: function(accountData) { let connection = { get host() { return "TbSync#" + accountData.accountID; }, get username() { return accountData.getAccountProperty("user"); }, get password() { // try new host first let pw = TbSync.passwordManager.getLoginInfo(this.host, "TbSync/DAV", this.username); if (pw) { return pw; } // try old host as fallback let oldHost = accountData.getAccountProperty("calDavHost") ? accountData.getAccountProperty("calDavHost") : accountData.getAccountProperty("cardDavHost"); if (oldHost.startsWith("http://")) oldHost = oldHost.substr(7); if (oldHost.startsWith("https://")) oldHost = oldHost.substr(8); pw = TbSync.passwordManager.getLoginInfo(oldHost, "TbSync/DAV", this.username); if (pw) { //migrate this.updateLoginData(this.username, pw); } return pw; }, updateLoginData: function(newUsername, newPassword) { let oldUsername = this.username; TbSync.passwordManager.updateLoginInfo(this.host, "TbSync/DAV", oldUsername, newUsername, newPassword); // Also update the username of this account. accountData.setAccountProperty("user", newUsername); }, removeLoginData: function() { TbSync.passwordManager.removeLoginInfos(this.host, "TbSync/DAV"); } }; return connection; }, // prepare and patch OAuth2 object getOAuthObj: function(_uri, configObject = null) { let uri = _uri; // if _uri input is not yet an uri, try to get one try { if (!_uri.spec) uri = Services.io.newURI(_uri); } catch (e) { Components.utils.reportError(e); return null; } let config = {}; switch (uri.host) { case "apidata.googleusercontent.com": case "www.googleapis.com": config = { base_uri : "https://accounts.google.com/o/", //redirect_uri : "urn:ietf:wg:oauth:2.0:oob:auto", scope : "https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar", client_id : "689460414096-e4nddn8tss5c59glidp4bc0qpeu3oper.apps.googleusercontent.com", client_secret : "LeTdF3UEpCvP1V3EBygjP-kl", } break; default: return null; } let oauth = new OAuth2(config.base_uri, config.scope, config.client_id, config.client_secret); oauth.requestWindowFeatures = "chrome,private,centerscreen,width=500,height=750"; //the v2 endpoints are different and would need manual override //oauth.authURI = //oauth.tokenURI = oauth.extraAuthParams = [ ["access_type", "offline"], ["prompt", "select_account"], // Does not work with "legacy" clients like Thunderbird, do not know why, // also the OAuth UI looks different from Firefox. //["login_hint", "test@gmail.com"], ]; // Storing the accountID as part of the URI has multiple benefits: // - it does not get lost during offline support disable/enable // - we can connect multiple google accounts without running into same-url-issue of shared calendars // - if called from lightning, we do not need to do an expensive url search to get the accountID let accountID = uri.username || ((configObject && configObject.hasOwnProperty("accountID")) ? configObject.accountID : null); let accountData = null; try { accountData = new TbSync.AccountData(accountID); } catch (e) {}; if (configObject && configObject.hasOwnProperty("accountname")) { oauth.requestWindowTitle = "TbSync account <" + configObject.accountname + "> requests authorization."; } else if (accountData) { oauth.requestWindowTitle = "TbSync account <" + accountData.getAccountProperty("accountname") + "> requests authorization."; } else { oauth.requestWindowTitle = "A TbSync account requests authorization."; } /* Adding custom methods to the oauth object */ // Similar to tbSyncDavCalendar.oauthConnect(), but true async. oauth.asyncConnect = async function(rv) { let self = this; rv.error = ""; rv.tokens = ""; // If multiple resources need to authenticate they will all end here, even though they // might share the same token. Due to the async nature, each process will refresh // "its own" token again, which is not needed. We force clear the token here and each // final connect process will actually check the acccessToken and abort the refresh, // if it is already there, generated by some other process. if (self.accessToken) self.accessToken = ""; try { await new Promise(function(resolve, reject) { // refresh = false will do nothing and resolve immediately, if an accessToken // exists already, which must have been generated by another process, as // we cleared it beforehand. self.connect(resolve, reject, /* with UI */ true, /* refresh */ false); }); rv.tokens = self.tokens; return true; } catch (e) { rv.error = e; } try { switch (JSON.parse(rv.error).error) { case "invalid_grant": self.accessToken = ""; self.refreshToken = ""; return true; case "cancelled": rv.error = "OAuthAbortError"; break; default: rv.error = "OAuthServerError::"+rv.error; break; } } catch (e) { rv.error = "OAuthServerError::"+rv.error; } return false; }; oauth.isExpired = function() { const OAUTH_GRACE_TIME = 30 * 1000; return (this.tokenExpires - OAUTH_GRACE_TIME < new Date().getTime()); }; const OAUTHVALUES = [ ["access", "", "accessToken"], ["refresh", "", "refreshToken"], ["expires", Number.MAX_VALUE, "tokenExpires"], ]; // returns a JSON string containing all the oauth values Object.defineProperty(oauth, "tokens", { get: function() { let tokensObj = {}; for (let oauthValue of OAUTHVALUES) { // use the system value or if not defined the default tokensObj[oauthValue[0]] = this[oauthValue[2]] || oauthValue[1]; } return JSON.stringify(tokensObj); }, enumerable: true, }); if (accountData) { // authData allows us to access the password manager values belonging to this account/calendar // simply by authdata.username and authdata.password oauth.authData = TbSync.providers.dav.network.getAuthData(accountData); oauth.parseAndSanitizeTokenString = function(tokenString) { let _tokensObj = {}; try { _tokensObj = JSON.parse(tokenString); } catch (e) {} let tokensObj = {}; for (let oauthValue of OAUTHVALUES) { // use the provided value or if not defined the default tokensObj[oauthValue[0]] = (_tokensObj && _tokensObj.hasOwnProperty(oauthValue[0])) ? _tokensObj[oauthValue[0]] : oauthValue[1]; } return tokensObj; }; // Define getter/setter to act on the password manager password value belonging to this account/calendar for (let oauthValue of OAUTHVALUES) { Object.defineProperty(oauth, oauthValue[2], { get: function() { return this.parseAndSanitizeTokenString(this.authData.password)[oauthValue[0]]; }, set: function(val) { let tokens = this.parseAndSanitizeTokenString(this.authData.password); let valueChanged = (val != tokens[oauthValue[0]]) if (valueChanged) { tokens[oauthValue[0]] = val; this.authData.updateLoginData(this.authData.username, JSON.stringify(tokens)); } }, enumerable: true, }); } } return oauth; }, getOAuthValue: function(currentTokenString, type = "access") { try { let tokens = JSON.parse(currentTokenString); if (tokens.hasOwnProperty(type)) return tokens[type]; } catch (e) { //NOOP } return ""; }, ConnectionData: class { constructor(data) { this._password = ""; this._username = ""; this._https = ""; this._type = ""; this._fqdn = ""; this._timeout = dav.Base.getConnectionTimeout(); //for error logging this._eventLogInfo = null; //typof syncdata? let folderData = null; let accountData = null; if (data instanceof TbSync.SyncData) { folderData = data.currentFolderData; accountData = data.accountData; this._eventLogInfo = data.eventLogInfo; } else if (data instanceof TbSync.FolderData) { folderData = data; accountData = data.accountData; this._eventLogInfo = new TbSync.EventLogInfo( accountData.getAccountProperty("provider"), accountData.getAccountProperty("accountname"), accountData.accountID, folderData.getFolderProperty("foldername")); } else if (data instanceof TbSync.AccountData) { accountData = data; this._eventLogInfo = new TbSync.EventLogInfo( accountData.getAccountProperty("provider"), accountData.getAccountProperty("accountname"), accountData.accountID, ""); } if (accountData) { let authData = dav.network.getAuthData(accountData); this._password = authData.password; this._username = authData.username; this._accountname = accountData.getAccountProperty("accountname"); if (folderData) { this._fqdn = folderData.getFolderProperty("fqdn"); this._https = folderData.getFolderProperty("https"); } this.accountData = accountData; } } set password(v) {this._password = v;} set username(v) {this._username = v;} set timeout(v) {this._timeout = v;} set https(v) {this._https = v;} set fqdn(v) {this._fqdn = v;} set eventLogInfo(v) {this._eventLogInfo = v;} get password() {return this._password;} get username() {return this._username;} get timeout() {return this._timeout;} get https() {return this._https;} get fqdn() {return this._fqdn;} get eventLogInfo() {return this._eventLogInfo;} }, checkForRFC6764Request: async function (path, eventLogInfo) { function checkDefaultSecPort (sec) { return sec ? "443" : "80"; } if (!this.isRFC6764Request(path)) { return path; } let parts = path.toLowerCase().split("6764://"); let type = parts[0].endsWith("caldav") ? "caldav" : "carddav"; // obey preselected security level for DNS lookup // and only use insecure option if specified let scheme = parts[0].startsWith("httpca") ? "http" : "https"; //httpcaldav or httpcarddav = httpca = http let sec = (scheme == "https"); let hostPath = parts[1]; while (hostPath.endsWith("/")) { hostPath = hostPath.slice(0,-1); } let host = hostPath.split("/")[0]; let result = {}; //only perform dns lookup, if the provided path does not contain any path information if (host == hostPath) { let request = "_" + type + (sec ? "s" : "") + "._tcp." + host; // get host from SRV record let rv = await DNS.srv(request); if (rv && Array.isArray(rv) && rv.length>0 && rv[0].host) { result.secure = sec; result.host = rv[0].host + ((checkDefaultSecPort(sec) == rv[0].port) ? "" : ":" + rv[0].port); TbSync.eventlog.add("info", eventLogInfo, "RFC6764 DNS request succeeded", "SRV record @ " + request + "\n" + JSON.stringify(rv[0])); // Now try to get path from TXT rv = await DNS.txt(request); if (rv && Array.isArray(rv) && rv.length>0 && rv[0].data && rv[0].data.toLowerCase().startsWith("path=")) { result.path = rv[0].data.substring(5); TbSync.eventlog.add("info", eventLogInfo, "RFC6764 DNS request succeeded", "TXT record @ " + request + "\n" + JSON.stringify(rv[0])); } else { result.path = "/.well-known/" + type; } result.url = "http" + (result.secure ? "s" : "") + "://" + result.host + result.path; return result.url; } else { TbSync.eventlog.add("warning", eventLogInfo, "RFC6764 DNS request failed", "SRV record @ " + request); } } // use the provided hostPath and build standard well-known url return scheme + "://" + hostPath + "/.well-known/" + type; }, startsWithScheme: function (url) { return (url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://") || this.isRFC6764Request(url)); }, isRFC6764Request: function (url) { let parts = url.split("6764://"); return (parts.length == 2 && parts[0].endsWith("dav")); }, sendRequest: async function (requestData, path, method, connectionData, headers = {}, options = {}) { let url = await this.checkForRFC6764Request(path, connectionData.eventLogInfo); let enforcedPermanentlyRedirectedUrl = (url != path) ? url : null; // path could be absolute or relative, we may need to rebuild the full url. if (url.startsWith("http://") || url.startsWith("https://")) { // extract segments from url let uri = Services.io.newURI(url); connectionData.https = (uri.scheme == "https"); connectionData.fqdn = uri.hostPort; } else { url = "http" + (connectionData.https ? "s" : "") + "://" + connectionData.fqdn + url; } let currentSyncState = connectionData.accountData ? connectionData.accountData.syncData.getSyncState().state : ""; let accountID = connectionData.accountData ? connectionData.accountData.accountID : ""; // Loop: Prompt user for password and retry const MAX_RETRIES = options.hasOwnProperty("passwordRetries") ? options.passwordRetries+1 : 5; for (let i=1; i <= MAX_RETRIES; i++) { TbSync.dump("URL Request #" + i, url); connectionData.url = url; connectionData.oauthObj = dav.network.getOAuthObj(connectionData.url, { username: connectionData.username, accountID }); // Check OAUTH situation before connecting. if (connectionData.oauthObj && (!connectionData.oauthObj.accessToken || connectionData.oauthObj.isExpired())) { let rv = {} if (connectionData.accountData) { connectionData.accountData.syncData.setSyncState("oauthprompt"); } if (await connectionData.oauthObj.asyncConnect(rv)) { connectionData.password = rv.tokens; } else { throw dav.sync.finish("error", rv.error); } } // Restore original syncstate before open the connection if (connectionData.accountData && currentSyncState != connectionData.accountData.syncData.getSyncState().state) { connectionData.accountData.syncData.setSyncState(currentSyncState); } let r = await dav.network.promisifiedHttpRequest(requestData, method, connectionData, headers, options); if (r && enforcedPermanentlyRedirectedUrl && !r.permanentlyRedirectedUrl) { r.permanentlyRedirectedUrl = enforcedPermanentlyRedirectedUrl; } if (r && r.passwordPrompt && r.passwordPrompt === true) { if (i == MAX_RETRIES) { // If this is the final retry, abort with error. throw r.passwordError; } else { let credentials = null; let retry = false; // Prompt, if connection belongs to an account (and not from the create wizard) if (connectionData.accountData) { if (connectionData.oauthObj) { connectionData.oauthObj.accessToken = ""; retry = true; } else { let promptData = { windowID: "auth:" + connectionData.accountData.accountID, accountname: connectionData.accountData.getAccountProperty("accountname"), usernameLocked: connectionData.accountData.isConnected(), username: connectionData.username, } connectionData.accountData.syncData.setSyncState("passwordprompt"); credentials = await TbSync.passwordManager.asyncPasswordPrompt(promptData, dav.openWindows); if (credentials) { // update login data dav.network.getAuthData(connectionData.accountData).updateLoginData(credentials.username, credentials.password); // update connection data connectionData.username = credentials.username; connectionData.password = credentials.password; retry = true; } } } if (!retry) { throw r.passwordError; } } } else { return r; } } }, // Promisified implementation of TbSync's HttpRequest (with XHR interface) promisifiedHttpRequest: function (requestData, method, connectionData, headers, options) { let responseData = ""; //do not log HEADERS, as it could contain an Authorization header //TbSync.dump("HEADERS", JSON.stringify(headers)); if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) TbSync.dump("REQUEST", method + " : " + requestData); if (!options.hasOwnProperty("softfail")) { options.softfail = []; } if (!options.hasOwnProperty("responseType")) { options.responseType = "xml"; } return new Promise(function(resolve, reject) { let req = new HttpRequest(); req.timeout = connectionData.timeout; req.mozBackgroundRequest = true; req.open(method, connectionData.url, true, connectionData.username, connectionData.password); if (options.hasOwnProperty("containerRealm")) req.setContainerRealm(options.containerRealm); if (options.hasOwnProperty("containerReset") && options.containerReset == true) req.clearContainerCache(); if (headers) { for (let header in headers) { req.setRequestHeader(header, headers[header]); } } if (options.responseType == "base64") { req.responseAsBase64 = true; } req.setRequestHeader("User-Agent", dav.sync.prefSettings.getCharPref("clientID.useragent")); // If this is one of the servers which we use OAuth for, add the bearer token. if (connectionData.oauthObj) { req.setRequestHeader("Authorization", "Bearer " + dav.network.getOAuthValue(connectionData.password, "access")); } req.realmCallback = function(username, realm, host) { // Store realm, needed later to setup lightning passwords. TbSync.dump("Found CalDAV authRealm for <"+host+">", realm); connectionData.realm = realm; }; req.onerror = function () { let error = TbSync.network.createTCPErrorFromFailedXHR(req); if (!error) { return reject(dav.sync.finish("error", "networkerror", "URL:\n" + connectionData.url + " ("+method+")")); //reject/resolve do not terminate control flow } else { return reject(dav.sync.finish("error", error, "URL:\n" + connectionData.url + " ("+method+")")); } }; req.ontimeout = req.onerror; req.onredirect = function(flags, uri) { console.log("Redirect ("+ flags.toString(2) +"): " + uri.spec); // Update connection settings from current URL let newHttps = (uri.scheme == "https"); if (connectionData.https != newHttps) { TbSync.dump("Updating HTTPS", connectionData.https + " -> " + newHttps); connectionData.https = newHttps; } if (connectionData.fqdn !=uri.hostPort) { TbSync.dump("Updating FQDN", connectionData.fqdn + " -> " + uri.hostPort); connectionData.fqdn = uri.hostPort; } }; req.onload = function() { if (TbSync.prefs.getIntPref("log.userdatalevel") > 1) TbSync.dump("RESPONSE", req.status + " ("+req.statusText+")" + " : " + req.responseText); responseData = req.responseText.split("><").join(">\n<"); let commLog = "URL:\n" + connectionData.url + " ("+method+")" + "\n\nRequest:\n" + requestData + "\n\nResponse:\n" + responseData; let aResult = req.responseText; let responseStatus = req.status; switch(responseStatus) { case 401: //AuthError { let response = {}; response.passwordPrompt = true; response.passwordError = dav.sync.finish("error", responseStatus, commLog); return resolve(response); } break; case 207: //preprocess multiresponse { let xml = dav.tools.convertToXML(aResult); if (xml === null) return reject(dav.sync.finish("warning", "malformed-xml", commLog)); let response = {}; response.davOptions = req.getResponseHeader("dav"); response.responseURL = req.responseURL; response.permanentlyRedirectedUrl = req.permanentlyRedirectedUrl; response.commLog = commLog; response.node = xml.documentElement; let multi = xml.documentElement.getElementsByTagNameNS(dav.sync.ns.d, "response"); response.multi = []; for (let i=0; i < multi.length; i++) { let hrefNode = dav.tools.evaluateNode(multi[i], [["d","href"]]); let responseStatusNode = dav.tools.evaluateNode(multi[i], [["d", "status"]]); let propstats = multi[i].getElementsByTagNameNS(dav.sync.ns.d, "propstat"); if (propstats.length > 0) { //response contains propstats, push each as single entry for (let p=0; p < propstats.length; p++) { let statusNode = dav.tools.evaluateNode(propstats[p], [["d", "status"]]); let resp = {}; resp.node = propstats[p]; resp.status = statusNode === null ? null : statusNode.textContent.split(" ")[1]; resp.responsestatus = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1]; resp.href = hrefNode === null ? null : hrefNode.textContent; response.multi.push(resp); } } else { //response does not contain any propstats, push raw response let resp = {}; resp.node = multi[i]; resp.status = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1]; resp.responsestatus = responseStatusNode === null ? null : responseStatusNode.textContent.split(" ")[1]; resp.href = hrefNode === null ? null : hrefNode.textContent; response.multi.push(resp); } } return resolve(response); } case 200: //returned by DELETE by radicale - watch this !!! return resolve(aResult); case 204: //is returned by DELETE - no data case 201: //is returned by CREATE - no data return resolve(null); break; default: if (options.softfail.includes(responseStatus)) { let noresponse = {}; noresponse.softerror = responseStatus; let xml = dav.tools.convertToXML(aResult); if (xml !== null) { let exceptionNode = dav.tools.evaluateNode(xml.documentElement, [["s","exception"]]); if (exceptionNode !== null) { noresponse.exception = exceptionNode.textContent; } } //manually log this non-fatal error TbSync.eventlog.add("info", connectionData.eventLogInfo, "softerror::"+responseStatus, commLog); return resolve(noresponse); } else { return reject(dav.sync.finish("warning", responseStatus, commLog)); } break; } }; req.send(requestData); }); } } DAV-4-TbSync-1.8/content/includes/sync.js000066400000000000000000001422471362346375200201540ustar00rootroot00000000000000/* /* * This file is part of DAV-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var sync = { finish: function (aStatus = "", msg = "", details = "") { let status = TbSync.StatusData.SUCCESS switch (aStatus) { case "": case "ok": status = TbSync.StatusData.SUCCESS; break; case "info": status = TbSync.StatusData.INFO; break; case "resyncAccount": status = TbSync.StatusData.ACCOUNT_RERUN; break; case "resyncFolder": status = TbSync.StatusData.FOLDER_RERUN; break; case "warning": status = TbSync.StatusData.WARNING; break; case "error": status = TbSync.StatusData.ERROR; break; default: console.log("TbSync/DAV: Unknown status <"+aStatus+">"); status = TbSync.StatusData.ERROR; break; } let e = new Error(); e.name = "dav4tbsync"; e.message = status.toUpperCase() + ": " + msg.toString() + " (" + details.toString() + ")"; e.statusData = new TbSync.StatusData(status, msg.toString(), details.toString()); return e; }, prefSettings: Services.prefs.getBranch("extensions.dav4tbsync."), ns: { d: "DAV:", cal: "urn:ietf:params:xml:ns:caldav" , card: "urn:ietf:params:xml:ns:carddav" , cs: "http://calendarserver.org/ns/", s: "http://sabredav.org/ns", apple: "http://apple.com/ns/ical/" }, serviceproviders: { "fruux" : {revision: 1, icon: "fruux", caldav: "https://dav.fruux.com", carddav: "https://dav.fruux.com"}, "mbo" : {revision: 1, icon: "mbo", caldav: "caldav6764://mailbox.org", carddav: "carddav6764://mailbox.org"}, "icloud" : {revision: 1, icon: "icloud", caldav: "https://caldav.icloud.com", carddav: "https://contacts.icloud.com"}, "google" : {revision: 1, icon: "google", caldav: "https://apidata.googleusercontent.com/caldav/v2/", carddav: "https://www.googleapis.com/.well-known/carddav"}, "gmx.net" : {revision: 1, icon: "gmx", caldav: "caldav6764://gmx.net", carddav: "carddav6764://gmx.net"}, "gmx.com" : {revision: 1, icon: "gmx", caldav: "caldav6764://gmx.com", carddav: "carddav6764://gmx.com"}, "posteo" : {revision: 1, icon: "posteo", caldav: "https://posteo.de:8443", carddav: "posteo.de:8843"}, "web.de" : {revision: 1, icon: "web", caldav: "caldav6764://web.de", carddav: "carddav6764://web.de"}, "yahoo" : {revision: 1, icon: "yahoo", caldav: "caldav6764://yahoo.com", carddav: "carddav6764://yahoo.com"}, }, onChange(abItem) { if (!this._syncOnChangeTimers) this._syncOnChangeTimers = {}; this._syncOnChangeTimers[abItem.abDirectory.UID] = {}; this._syncOnChangeTimers[abItem.abDirectory.UID].timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); this._syncOnChangeTimers[abItem.abDirectory.UID].event = { notify: function(timer) { // if account is syncing, re-schedule // if folder got synced after the start time (due to re-scheduling) abort console.log("DONE: "+ abItem.abDirectory.UID); } } this._syncOnChangeTimers[abItem.abDirectory.UID].timer.initWithCallback( this._syncOnChangeTimers[abItem.abDirectory.UID].event, 2000, Components.interfaces.nsITimer.TYPE_ONE_SHOT); }, resetFolderSyncInfo : function (folderData) { folderData.resetFolderProperty("ctag"); folderData.resetFolderProperty("token"); folderData.setFolderProperty("createdWithProviderVersion", folderData.accountData.providerData.getVersion()); }, folderList: async function (syncData) { //Method description: http://sabre.io/dav/building-a-caldav-client/ //get all folders currently known let folderTypes = ["caldav", "carddav", "ics"]; let unhandledFolders = {}; for (let type of folderTypes) { unhandledFolders[type] = []; } let folders = syncData.accountData.getAllFolders(); for (let folder of folders) { //just in case if (!unhandledFolders.hasOwnProperty(folder.getFolderProperty("type"))) { unhandledFolders[folder.getFolderProperty("type")] = []; } unhandledFolders[folder.getFolderProperty("type")].push(folder); } // refresh urls of service provider, if they have been updated let serviceprovider = syncData.accountData.getAccountProperty("serviceprovider"); let serviceproviderRevision = syncData.accountData.getAccountProperty("serviceproviderRevision"); if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider) && serviceproviderRevision != dav.sync.serviceproviders[serviceprovider].revision) { TbSync.eventlog.add("info", syncData.eventLogInfo, "updatingServiceProvider", serviceprovider); syncData.accountData.setAccountProperty("serviceproviderRevision", dav.sync.serviceproviders[serviceprovider].revision); syncData.accountData.resetAccountProperty("calDavPrincipal"); syncData.accountData.resetAccountProperty("cardDavPrincipal"); syncData.accountData.setAccountProperty("calDavHost", dav.sync.serviceproviders[serviceprovider].caldav); syncData.accountData.setAccountProperty("cardDavHost", dav.sync.serviceproviders[serviceprovider].carddav); } let davjobs = { cal : {server: syncData.accountData.getAccountProperty("calDavHost")}, card : {server: syncData.accountData.getAccountProperty("cardDavHost")}, }; for (let job in davjobs) { if (!davjobs[job].server) continue; // SOGo needs some special handling for shared addressbooks. We detect it by having SOGo/dav in the url. let isSogo = davjobs[job].server.includes("/SOGo/dav"); //sync states are only printed while the account state is "syncing" to inform user about sync process (it is not stored in DB, just in syncData) //example state "getfolders" to get folder information from server //if you send a request to a server and thus have to wait for answer, use a "send." syncstate, which will give visual feedback to the user, //that we are waiting for an answer with timeout countdown let home = []; let own = []; // migration code for http setting, we might keep it as a fallback, if user removed the http:// scheme from the url in the settings if (!dav.network.startsWithScheme(davjobs[job].server)) { davjobs[job].server = "http" + (syncData.accountData.getAccountProperty("https") ? "s" : "") + "://" + davjobs[job].server; syncData.accountData.setAccountProperty(job + "DavHost", davjobs[job].server); } //add connection to syncData syncData.connectionData = new dav.network.ConnectionData(syncData); //only do that, if a new calendar has been enabled TbSync.network.resetContainerForUser(syncData.connectionData.username); syncData.setSyncState("send.getfolders"); let principal = syncData.accountData.getAccountProperty(job + "DavPrincipal"); // defaults to null if (principal === null) { let response = await dav.network.sendRequest("", davjobs[job].server , "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"}); syncData.setSyncState("eval.folders"); // keep track of permanent redirects for the server URL if (response && response.permanentlyRedirectedUrl) { syncData.accountData.setAccountProperty(job + "DavHost", response.permanentlyRedirectedUrl) } // store dav options send by server if (response && response.davOptions) { syncData.accountData.setAccountProperty(job + "DavOptions", response.davOptions.split(",").map(e => e.trim())); } // allow 404 because iCloud sends it on valid answer (yeah!) if (response && response.multi) { principal = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d","current-user-principal"], ["d","href"]], null, ["200","404"]); } } //principal now contains something like "/remote.php/carddav/principals/john.bieling/" //principal can also be an absolute url // -> get home/root of storage if (principal !== null) { syncData.setSyncState("send.getfolders"); let options = syncData.accountData.getAccountProperty(job + "DavOptions"); let homeset = (job == "cal") ? "calendar-home-set" : "addressbook-home-set"; let request = "<"+job+":" + homeset + " />" + (job == "cal" && options.includes("calendar-proxy") ? "" : "") + "" + ""; let response = await dav.network.sendRequest(request, principal, "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"}); syncData.setSyncState("eval.folders"); // keep track of permanent redirects for the principal URL if (response && response.permanentlyRedirectedUrl) { principal = response.permanentlyRedirectedUrl; } own = dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], [job, homeset ], ["d","href"]], principal); home = own.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["cs", "calendar-proxy-read-for" ], ["d","href"]], principal)); home = home.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["cs", "calendar-proxy-write-for" ], ["d","href"]], principal)); //Any groups we need to find? Only diving one level at the moment, let g = dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], ["d", "group-membership" ], ["d","href"]], principal); for (let gc=0; gc < g.length; gc++) { //SOGo reports a 403 if I request the provided resource, also since we do not dive, remove the request for group-membership response = await dav.network.sendRequest(request.replace("",""), g[gc], "PROPFIND", syncData.connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {softfail: [403, 404]}); if (response && response.softerror) { continue; } home = home.concat(dav.tools.getNodesTextContentFromMultiResponse(response, [["d","prop"], [job, homeset ], ["d","href"]], g[gc])); } //calendar-proxy and group-membership could have returned the same values, make the homeset unique home = home.filter((v,i,a) => a.indexOf(v) == i); } else { // do not throw here, but log the error and skip this server TbSync.eventlog.add("error", syncData.eventLogInfo, job+"davservernotfound", davjobs[job].server); } //home now contains something like /remote.php/caldav/calendars/john.bieling/ // -> get all resources if (home.length > 0) { // the used principal returned valid resources, store/update it // as the principal is being used as a starting point, it must be stored as absolute url syncData.accountData.setAccountProperty(job + "DavPrincipal", dav.network.startsWithScheme(principal) ? principal : "http" + (syncData.connectionData.https ? "s" : "") + "://" + syncData.connectionData.fqdn + principal); for (let h=0; h < home.length; h++) { syncData.setSyncState("send.getfolders"); let request = (job == "cal") ? "" : ""; //some servers report to have calendar-proxy-read but return a 404 when that gets actually queried let response = await dav.network.sendRequest(request, home[h], "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}, {softfail: [403, 404]}); if (response && response.softerror) { continue; } for (let r=0; r < response.multi.length; r++) { if (response.multi[r].status != "200") continue; let resourcetype = null; //is this a result with a valid recourcetype? (the node must be present) switch (job) { case "card": if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["card", "addressbook"]]) !== null) resourcetype = "carddav"; break; case "cal": if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["cal", "calendar"]]) !== null) resourcetype = "caldav"; else if (dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","resourcetype"], ["cs", "subscribed"]]) !== null) resourcetype = "ics"; break; } if (resourcetype === null) continue; //get ACL (grant read rights per default, if it is SOGo, as they do not send that permission) let acl = isSogo ? 0x1 : 0; let privilegNode = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","current-user-privilege-set"]]); if (privilegNode) { if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "all").length > 0) { acl = 0xF; //read=1, mod=2, create=4, delete=8 } else { // check for individual write permissions if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "write").length > 0) { acl = 0xF; } else { if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "write-content").length > 0) acl |= 0x2; if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "bind").length > 0) acl |= 0x4; if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "unbind").length > 0) acl |= 0x8; } // check for read permission (implying read if any write is given) if (privilegNode.getElementsByTagNameNS(dav.sync.ns.d, "read").length > 0 || acl != 0) acl |= 0x1; } } //ignore this resource, if no read access if ((acl & 0x1) == 0) continue; let href = response.multi[r].href; if (resourcetype == "ics") href = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["cs","source"], ["d","href"]]).textContent; let name_node = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["d","displayname"]]); let name = TbSync.getString("defaultname." + ((job == "cal") ? "calendar" : "contacts") , "dav"); if (name_node != null) { name = name_node.textContent; } let color = dav.tools.evaluateNode(response.multi[r].node, [["d","prop"], ["apple","calendar-color"]]); //remove found folder from list of unhandled folders unhandledFolders[resourcetype] = unhandledFolders[resourcetype].filter(item => item.getFolderProperty("href") !== href); // interaction with TbSync // do we have a folder for that href? let folderData = syncData.accountData.getFolder("href", href); if (!folderData) { // create a new folder entry folderData = syncData.accountData.createNewFolder(); // this MUST be set to either "addressbook" or "calendar" to use the standard target support, or any other value, which // requires a corresponding targets implementation by this provider folderData.setFolderProperty("targetType", (job == "card") ? "addressbook" : "calendar"); folderData.setFolderProperty("href", href); folderData.setFolderProperty("foldername", name); folderData.setFolderProperty("type", resourcetype); folderData.setFolderProperty("shared", !own.includes(home[h])); folderData.setFolderProperty("acl", acl.toString()); folderData.setFolderProperty("downloadonly", (acl == 0x1)); //if any write access is granted, setup as writeable //we assume the folder has the same fqdn as the homeset, otherwise href must contain the full URL and the fqdn is ignored folderData.setFolderProperty("fqdn", syncData.connectionData.fqdn); folderData.setFolderProperty("https", syncData.connectionData.https); //do we have a cached folder? let cachedFolderData = syncData.accountData.getFolderFromCache("href", href); if (cachedFolderData) { // copy fields from cache which we want to re-use folderData.setFolderProperty("targetColor", cachedFolderData.getFolderProperty("targetColor")); folderData.setFolderProperty("targetName", cachedFolderData.getFolderProperty("targetName")); //if we have only READ access, do not restore cached value for downloadonly if (acl > 0x1) folderData.setFolderProperty("downloadonly", cachedFolderData.getFolderProperty("downloadonly")); } } else { //Update name & color folderData.setFolderProperty("foldername", name); folderData.setFolderProperty("fqdn", syncData.connectionData.fqdn); folderData.setFolderProperty("https", syncData.connectionData.https); folderData.setFolderProperty("acl", acl); //if the acl changed from RW to RO we need to update the downloadonly setting if (acl == 0x1) { folderData.setFolderProperty("downloadonly", true); } } // Update color from server (skip if nolightning, no // need to run into error when hasTarget() throws). if (color && job == "cal" && TbSync.lightning.isAvailable()) { color = color.textContent.substring(0,7); folderData.setFolderProperty("targetColor", color); // Do we have to update the calendar? if (folderData.targetData && folderData.targetData.hasTarget()) { try { let targetCal = await folderData.targetData.getTarget(); targetCal.calendar.setProperty("color", color); } catch (e) { Components.utils.reportError(e) } } } } } } else { //home was not found - connection error? - do not delete unhandled folders switch (job) { case "card": unhandledFolders.carddav = []; break; case "cal": unhandledFolders.caldav = []; unhandledFolders.ics = []; break; } //reset stored principal syncData.accountData.resetAccountProperty(job + "DavPrincipal"); } } // Remove unhandled old folders, (because they no longer exist on the server). // Do not delete the targets, but keep them as stale/unconnected elements. for (let type of folderTypes) { for (let folder of unhandledFolders[type]) { folder.remove("[deleted on server]"); } } }, folder: async function (syncData) { // add connection data to syncData syncData.connectionData = new dav.network.ConnectionData(syncData); // add target to syncData (getTarget() will throw "nolightning" if lightning missing) let hadTarget; try { // accessing the target for the first time will check if it is avail and if not will create it (if possible) hadTarget = syncData.currentFolderData.targetData.hasTarget(); syncData.target = await syncData.currentFolderData.targetData.getTarget(); } catch (e) { Components.utils.reportError(e); throw dav.sync.finish("warning", e.message); } switch (syncData.currentFolderData.getFolderProperty("type")) { case "carddav": { await dav.sync.singleFolder(syncData); } break; case "caldav": case "ics": { // update downloadonly - we do not use TbCalendar (syncData.target) but the underlying lightning calendar obj if (syncData.currentFolderData.getFolderProperty("downloadonly")) syncData.target.calendar.setProperty("readOnly", true); // update username of calendar syncData.target.calendar.setProperty("username", syncData.connectionData.username); //init sync via lightning if (hadTarget) syncData.target.calendar.refresh(); throw dav.sync.finish("ok", "managed-by-lightning"); } break; default: { throw dav.sync.finish("warning", "notsupported"); } break; } }, singleFolder: async function (syncData) { let downloadonly = syncData.currentFolderData.getFolderProperty("downloadonly"); // we have to abort sync of this folder, if it is contact, has groupSync enabled and gContactSync is enabled let syncGroups = syncData.accountData.getAccountProperty("syncGroups"); let gContactSync = await AddonManager.getAddonByID("gContactSync@pirules.net") ; let contactSync = (syncData.currentFolderData.getFolderProperty("type") == "carddav"); if (syncGroups && contactSync && gContactSync && gContactSync.isActive) { throw dav.sync.finish("warning", "gContactSync"); } await dav.sync.remoteChanges(syncData); let numOfLocalChanges = await dav.sync.localChanges(syncData); //revert all local changes on permission error by doing a clean sync if (numOfLocalChanges < 0) { dav.sync.resetFolderSyncInfo(syncData.currentFolderData); await dav.sync.remoteChanges(syncData); if (!downloadonly) throw dav.sync.finish("info", "info.restored"); } else if (numOfLocalChanges > 0){ //we will get back our own changes and can store etags and vcards and also get a clean ctag/token await dav.sync.remoteChanges(syncData); } }, remoteChanges: async function (syncData) { //Do we have a sync token? No? -> Initial Sync (or WebDAV sync not supported) / Yes? -> Get updates only (token only present if WebDAV sync is suported) let token = syncData.currentFolderData.getFolderProperty("token"); let isGoogle = (syncData.accountData.getAccountProperty("serviceprovider") == "google"); if (token && !isGoogle) { //update via token sync let tokenSyncSucceeded = await dav.sync.remoteChangesByTOKEN(syncData); if (tokenSyncSucceeded) return; //token sync failed, reset ctag and token and do a full sync dav.sync.resetFolderSyncInfo(syncData.currentFolderData); } //Either token sync did not work or there is no token (initial sync) //loop until ctag is the same before and after polling data (sane start condition) let maxloops = 20; for (let i=0; i <= maxloops; i++) { if (i == maxloops) throw dav.sync.finish("warning", "could-not-get-stable-ctag"); let ctagChanged = await dav.sync.remoteChangesByCTAG(syncData); if (!ctagChanged) break; } }, remoteChangesByTOKEN: async function (syncData) { syncData.progressData.reset(); let token = syncData.currentFolderData.getFolderProperty("token"); syncData.setSyncState("send.request.remotechanges"); let cards = await dav.network.sendRequest(""+token+"1", syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {}, {softfail: [415,403,409]}); //EteSync throws 409 because it does not support sync-token //Sabre\DAV\Exception\ReportNotSupported - Unsupported media type - returned by fruux if synctoken is 0 (empty book), 415 & 403 //https://github.com/sabre-io/dav/issues/1075 //Sabre\DAV\Exception\InvalidSyncToken (403) if (cards && cards.softerror) { //token sync failed, reset ctag and do a full sync return false; } let tokenNode = dav.tools.evaluateNode(cards.node, [["d", "sync-token"]]); if (tokenNode === null) { //token sync failed, reset ctag and do a full sync return false; } let vCardsDeletedOnServer = []; let vCardsChangedOnServer = {}; let localDeletes = syncData.target.getDeletedItemsFromChangeLog(); let cardsFound = 0; for (let c=0; c < cards.multi.length; c++) { let id = cards.multi[c].href; if (id !==null) { //valid let card = syncData.target.getItemFromProperty("X-DAV-HREF", id); if (cards.multi[c].status == "200") { //MOD or ADD let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]); if (!card) { //if the user deleted this card (not yet send to server), do not add it again if (!localDeletes.includes(id)) { cardsFound++; vCardsChangedOnServer[id] = "ADD"; } } else if (etag.textContent != card.getProperty("X-DAV-ETAG")) { cardsFound++; vCardsChangedOnServer[id] = "MOD"; } } else if (cards.multi[c].responsestatus == "404" && card) { //DEL cardsFound++; vCardsDeletedOnServer.push(card); } else { //We received something, that is not a DEL, MOD or ADD TbSync.eventlog.add("warning", syncData.eventLogInfo, "Unknown XML", JSON.stringify(cards.multi[c])); } } } // reset sync process syncData.progressData.reset(0, cardsFound); //download all cards added to vCardsChangedOnServer and process changes await dav.sync.multiget(syncData, vCardsChangedOnServer); //delete all contacts added to vCardsDeletedOnServer await dav.sync.deleteContacts (syncData, vCardsDeletedOnServer); //update token syncData.currentFolderData.setFolderProperty("token", tokenNode.textContent); return true; }, remoteChangesByCTAG: async function (syncData) { syncData.progressData.reset(); //Request ctag and token syncData.setSyncState("send.request.remotechanges"); let response = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "0"}); syncData.setSyncState("eval.response.remotechanges"); let ctag = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["cs", "getctag"]], syncData.currentFolderData.getFolderProperty("href")); let token = dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d", "sync-token"]], syncData.currentFolderData.getFolderProperty("href")); let localDeletes = syncData.target.getDeletedItemsFromChangeLog(); //if CTAG changed, we need to sync everything and compare if (ctag === null || ctag != syncData.currentFolderData.getFolderProperty("ctag")) { let vCardsFoundOnServer = []; let vCardsChangedOnServer = {}; //get etags of all cards on server and find the changed cards syncData.setSyncState("send.request.remotechanges"); let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}); //to test other impl //let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}, {softfail: []}, false); //this is the same request, but includes getcontenttype, do we need it? icloud send contacts without //let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}); //play with filters and limits for synology /* let additional = "10"; additional += ""; additional += ""; additional += "bogusxy"; additional += ""; additional += "";*/ //addressbook-query does not work on older servers (zimbra) //let cards = await dav.network.sendRequest("", syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"}); syncData.setSyncState("eval.response.remotechanges"); let cardsFound = 0; for (let c=0; cards.multi && c < cards.multi.length; c++) { let id = cards.multi[c].href; if (id == syncData.currentFolderData.getFolderProperty("href")) { //some servers (Radicale) report the folder itself and a querry to that would return everything again continue; } let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]); //ctype is currently not used, because iCloud does not send one and sabre/dav documentation is not checking ctype //let ctype = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getcontenttype"]]); if (cards.multi[c].status == "200" && etag !== null && id !== null /* && ctype !== null */) { //we do not actually check the content of ctype - but why do we request it? iCloud seems to send cards without ctype vCardsFoundOnServer.push(id); let card = syncData.target.getItemFromProperty("X-DAV-HREF", id); if (!card) { //if the user deleted this card (not yet send to server), do not add it again if (!localDeletes.includes(id)) { cardsFound++; vCardsChangedOnServer[id] = "ADD"; } } else if (etag.textContent != card.getProperty("X-DAV-ETAG")) { cardsFound++; vCardsChangedOnServer[id] = "MOD"; } } } //FIND DELETES: loop over current addressbook and check each local card if it still exists on the server let vCardsDeletedOnServer = []; let localAdditions = syncData.target.getAddedItemsFromChangeLog(); let allItems = syncData.target.getAllItems() for (let card of allItems) { let id = card.getProperty("X-DAV-HREF"); if (id && !vCardsFoundOnServer.includes(id) && !localAdditions.includes(id)) { //delete request from server cardsFound++; vCardsDeletedOnServer.push(card); } } // reset sync process syncData.progressData.reset(0, cardsFound); //download all cards added to vCardsChangedOnServer and process changes await dav.sync.multiget(syncData, vCardsChangedOnServer); //delete all contacts added to vCardsDeletedOnServer await dav.sync.deleteContacts (syncData, vCardsDeletedOnServer); //update ctag and token (if there is one) if (ctag === null) return false; //if server does not support ctag, "it did not change" syncData.currentFolderData.setFolderProperty("ctag", ctag); if (token) syncData.currentFolderData.setFolderProperty("token", token); //ctag did change return true; } else { //ctag did not change return false; } }, multiget: async function (syncData, vCardsChangedOnServer) { //keep track of found mailing lists and its members syncData.foundMailingListsDuringDownSync = {}; //download all changed cards and process changes let cards2catch = Object.keys(vCardsChangedOnServer); let maxitems = dav.sync.prefSettings.getIntPref("maxitems"); for (let i=0; i < cards2catch.length; i+=maxitems) { let request = dav.tools.getMultiGetRequest(cards2catch.slice(i, i+maxitems)); if (request) { syncData.setSyncState("send.request.remotechanges"); let cards = await dav.network.sendRequest(request, syncData.currentFolderData.getFolderProperty("href"), "REPORT", syncData.connectionData, {"Depth": "1"}); syncData.setSyncState("eval.response.remotechanges"); for (let c=0; c < cards.multi.length; c++) { syncData.progressData.inc(); let id = cards.multi[c].href; let etag = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["d","getetag"]]); let data = dav.tools.evaluateNode(cards.multi[c].node, [["d","prop"], ["card","address-data"]]); if (cards.multi[c].status == "200" && etag !== null && data !== null && id !== null && vCardsChangedOnServer.hasOwnProperty(id)) { switch (vCardsChangedOnServer[id]) { case "ADD": await dav.tools.addContact (syncData, id, data, etag); break; case "MOD": await dav.tools.modifyContact (syncData, id, data, etag); break; } //Feedback from users: They want to see the individual count syncData.setSyncState("eval.response.remotechanges"); await TbSync.tools.sleep(100); } else { TbSync.dump("Skipped Card", [id, cards.multi[c].status == "200", etag !== null, data !== null, id !== null, vCardsChangedOnServer.hasOwnProperty(id)].join(", ")); } } } } // Feedback from users: They want to see the final count. syncData.setSyncState("eval.response.remotechanges"); await TbSync.tools.sleep(200); // On down sync, mailinglists need to be done at the very end so all member data is avail. if (syncData.accountData.getAccountProperty("syncGroups")) { let l=0; for (let listID in syncData.foundMailingListsDuringDownSync) { if (syncData.foundMailingListsDuringDownSync.hasOwnProperty(listID)) { l++; let list = syncData.target.getItemFromProperty("X-DAV-HREF", listID); if (!list.isMailList) continue; let currentMembers = list.getMembersPropertyList("X-DAV-UID"); //CardInfo contains the name and the X-DAV-UID list of the members let vCardInfo = dav.tools.getGroupInfoFromCardData(syncData.foundMailingListsDuringDownSync[listID].vCardData, syncData.target); let oCardInfo = dav.tools.getGroupInfoFromCardData(syncData.foundMailingListsDuringDownSync[listID].oCardData, syncData.target); // Smart merge: oCardInfo contains the state during last sync, vCardInfo is the current state. // By comparing we can learn, which member was deleted by the server (in old but not in new). let removedMembers = oCardInfo.members.filter(e => !vCardInfo.members.includes(e)); // The new list from the server is taken. let newMembers = vCardInfo.members; // Any member in current but not in new is added. for (let member of currentMembers) { if (!newMembers.includes(member) && !removedMembers.includes(member)) newMembers.push(member); } // Remove local deletes. for (let member of oCardInfo.members) { if (!currentMembers.includes(member)) newMembers = newMembers.filter(e => e != member); } // Check that all new members have an email address (fix for bug 1522453) let m=0; for (let member of newMembers) { let card = syncData.target.getItemFromProperty("X-DAV-UID", member); if (card) { let email = card.getProperty("PrimaryEmail"); if (!email) { let email = Date.now() + "." + l + "." + m + "@bug1522453"; card.setProperty("PrimaryEmail", email); syncData.target.modifyItem(card); } } else { TbSync.dump("Member not found: " + member); } m++; } list.setMembersByPropertyList("X-DAV-UID", newMembers); } } } }, deleteContacts: async function (syncData, cards2delete) { let maxitems = dav.sync.prefSettings.getIntPref("maxitems"); // try to show a progress based on maxitens during delete and not delete all at once for (let i=0; i < cards2delete.length; i+=maxitems) { //get size of next block let remain = (cards2delete.length - i); let chunk = Math.min(remain, maxitems); syncData.progressData.inc(chunk); syncData.setSyncState("eval.response.remotechanges"); await TbSync.tools.sleep(200); //we want the user to see, that deletes are happening for (let j=0; j < chunk; j++) { syncData.target.deleteItem(cards2delete[i+j]); } } }, localChanges: async function (syncData) { //define how many entries can be send in one request let maxitems = dav.sync.prefSettings.getIntPref("maxitems"); let downloadonly = syncData.currentFolderData.getFolderProperty("downloadonly"); let permissionErrors = 0; let permissionError = { //keep track of permission errors - preset with downloadonly status to skip sync in that case "added_by_user": downloadonly, "modified_by_user": downloadonly, "deleted_by_user": downloadonly }; let syncGroups = syncData.accountData.getAccountProperty("syncGroups"); //access changelog to get local modifications (done and todo are used for UI to display progress) syncData.progressData.reset(0, syncData.target.getItemsFromChangeLog().length); do { syncData.setSyncState("prepare.request.localchanges"); //get changed items from ChangeLog let changes = syncData.target.getItemsFromChangeLog(maxitems); if (changes.length == 0) break; for (let i=0; i < changes.length; i++) { switch (changes[i].status) { case "added_by_user": case "modified_by_user": { let isAdding = (changes[i].status == "added_by_user"); if (!permissionError[changes[i].status]) { //if this operation failed already, do not retry let card = syncData.target.getItem(changes[i].itemId); if (card) { if (card.isMailList && !syncGroups) { // Conditionally break out of the switch early, but do // execute the cleanup code below the switch. A continue would // miss that. break; } let vcard = card.isMailList ? dav.tools.getVCardFromThunderbirdListCard(syncData, card, isAdding) : dav.tools.getVCardFromThunderbirdContactCard(syncData, card, isAdding); let headers = {"Content-Type": "text/vcard; charset=utf-8"}; //if (!isAdding) headers["If-Match"] = vcard.etag; syncData.setSyncState("send.request.localchanges"); if (isAdding || vcard.modified) { let response = await dav.network.sendRequest(vcard.data, card.getProperty("X-DAV-HREF"), "PUT", syncData.connectionData, headers, {softfail: [403,405]}); syncData.setSyncState("eval.response.localchanges"); if (response && response.softerror) { permissionError[changes[i].status] = true; TbSync.eventlog.add("warning", syncData.eventLogInfo, "missing-permission::" + TbSync.getString(isAdding ? "acl.add" : "acl.modify", "dav")); } } } else { TbSync.eventlog.add("warning", syncData.eventLogInfo, "cardnotfoundbutinchangelog::" + changes[i].itemId + "/" + changes[i].status); } } if (permissionError[changes[i].status]) { //we where not allowed to add or modify that card, remove it, we will get a fresh copy on the following revert let card = syncData.target.getItem(changes[i].itemId); if (card) syncData.target.deleteItem(card); permissionErrors++; } } break; case "deleted_by_user": { if (!permissionError[changes[i].status]) { //if this operation failed already, do not retry syncData.setSyncState("send.request.localchanges"); let response = await dav.network.sendRequest("", changes[i].itemId , "DELETE", syncData.connectionData, {}, {softfail: [403, 404, 405]}); syncData.setSyncState("eval.response.localchanges"); if (response && response.softerror) { if (response.softerror != 404) { //we cannot do anything about a 404 on delete, the card has been deleted here and is not avail on server permissionError[changes[i].status] = true; TbSync.eventlog.add("warning", syncData.eventLogInfo, "missing-permission::" + TbSync.getString("acl.delete", "dav")); } } } if (permissionError[changes[i].status]) { permissionErrors++; } } break; } syncData.target.removeItemFromChangeLog(changes[i].itemId); syncData.progressData.inc(); //UI feedback } } while (true); //return number of modified cards or the number of permission errors (negativ) return (permissionErrors > 0 ? 0 - permissionErrors : syncData.progressData.done); }, } DAV-4-TbSync-1.8/content/includes/tbSyncDavCalendar.js000066400000000000000000000172531362346375200225250ustar00rootroot00000000000000/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { cal } = ChromeUtils.import("resource://calendar/modules/calUtils.jsm"); Cu.importGlobalProperties(["TextDecoder"]); Services.scriptloader.loadSubScript("resource://calendar/components/calDavCalendar.js", this); Services.scriptloader.loadSubScript("resource://calendar/calendar-js/calDavRequestHandlers.js", this); // // tbSyncDavCalendar.js // function tbSyncDavCalendar() { calDavCalendar.call(this); this.tbSyncLoaded = false; } tbSyncDavCalendar.prototype = { __proto__: calDavCalendar.prototype, classID: Components.ID('7eb8f992-3956-4607-95ac-b860ebd51f5a}'), classDescription: 'tbSyncCalDav', contractID: '@mozilla.org/calendar/calendar;1?type=tbSyncCalDav', sleep: function(delay) { let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); return new Promise(function(resolve, reject) { let event = { notify: function(timer) { resolve(); } } timer.initWithCallback(event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT); }); }, /** The following functions are almost copied 1-to-1 but needed little changes to work with tbSyncCalDav **/ getProperty: function(aName) { if (aName in this.mACLProperties && this.mACLProperties[aName]) { return this.mACLProperties[aName]; } switch (aName) { case "organizerId": if (this.calendarUserAddress) { return this.calendarUserAddress; } // else use configured email identity break; case "organizerCN": return null; // xxx todo case "itip.transport": if (this.hasAutoScheduling || this.hasScheduling) { return this.QueryInterface(Ci.calIItipTransport); } // else use outbound email-based iTIP (from cal.provider.BaseClass) break; case "capabilities.tasks.supported": return this.supportedItemTypes.includes("VTODO"); case "capabilities.events.supported": return this.supportedItemTypes.includes("VEVENT"); case "capabilities.autoschedule.supported": return this.hasAutoScheduling; case "capabilities.username.supported": return true; } // We needed to add one more __proto__. return this.__proto__.__proto__.__proto__.getProperty.apply(this, arguments); }, get type() { return "tbSyncCalDav"; }, firstInRealm: function() { let calendars = cal.getCalendarManager().getCalendars({}); for (let i = 0; i < calendars.length; i++) { if (calendars[i].type != "tbSyncCalDav" || calendars[i].getProperty("disabled")) { continue; } // XXX We should probably expose the inner calendar via an // interface, but for now use wrappedJSObject. let calendar = calendars[i].wrappedJSObject; if (calendar.mUncachedCalendar) { calendar = calendar.mUncachedCalendar; } if (calendar.uri.prePath == this.uri.prePath && calendar.authRealm == this.mAuthRealm) { if (calendar.id == this.id) { return true; } break; } } return false; }, /** Overriding lightning oauth **/ oauthConnect: function(authSuccessCb, authFailureCb, aRefresh = false) { let self = this; // If multiple resources need to authenticate they will all end here, even though they // might share the same token. Due to the async nature, each process will refresh // "its own" token again, which is not needed. We force clear the token here and each // final connect process will actually check the acccessToken and abort the refresh, // if it is already there, generated by some other process. // The value of aRefresh is being ignored. if (self.oauth.accessToken) self.oauth.accessToken = ""; // Use the async prompter to avoid multiple master password prompts let promptlistener = { onPromptStartAsync: function(callback) { this.onPromptAuthAvailable(callback); }, onPromptAuthAvailable: function(callback) { // refresh = false will do nothing and resolve immediately, if an accessToken // exists already, which must have been generated by another process, as // we cleared it beforehand. self.oauth.connect( () => { authSuccessCb(); if (callback) { callback.onAuthResult(true); } }, () => { authFailureCb(); if (callback) { callback.onAuthResult(false); } }, true, // with UI false // refresh ); }, onPromptCanceled: authFailureCb, onPromptStart: function() {}, }; let asyncprompter = Cc["@mozilla.org/messenger/msgAsyncPrompter;1"].getService( Ci.nsIMsgAsyncPrompter ); asyncprompter.queueAsyncAuthPrompt(self.uri.spec, false, promptlistener); }, setupAuthentication: async function(aChangeLogListener) { let self = this; function authSuccess() { self.checkDavResourceType(aChangeLogListener); } function authFailed() { self.setProperty("disabled", "true"); self.setProperty("auto-enabled", "true"); self.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_FAILURE); } // If TbSync is not installed, disable all calendars. let tbSyncAddon = await AddonManager.getAddonByID("tbsync@jobisoft.de"); if (!tbSyncAddon || !tbSyncAddon.isActive) { console.log("Failed to load TbSync, GoogleDav calendar will be disabled."); authFailed(); return; } // Wait until TbSync has been loaded for (let waitCycles=0; waitCycles < 120 && !this.tbSyncLoaded; waitCycles++) { await this.sleep(1000); try { var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); this.tbSyncLoaded = TbSync.enabled; } catch (e) { // If this fails, TbSync is not loaded yet. } } if (!this.tbSyncLoaded) { console.log("Failed to load TbSync, GoogleDav calendar will be disabled."); authFailed(); return; } // Wait until master password has been entered (if needed) while (!Services.logins.isLoggedIn) { await this.sleep(1000); } if (!this.oauth) { let oauth = TbSync.providers.dav.network.getOAuthObj(this.mUri); if (oauth) { // This Server req OAUTH this.oauth = oauth; } else { // This Server does not req OAUTH Server authSuccess(); return; } } if (this.oauth.accessToken) { authSuccess(); } else { // bug 901329: If the calendar window isn't loaded yet the // master password prompt will show just the buttons and // possibly hang. If we postpone until the window is loaded, // all is well. setTimeout(function postpone() { // eslint-disable-line func-names let win = cal.window.getCalendarWindow(); if (!win || win.document.readyState != "complete") { setTimeout(postpone, 0); } else { self.oauthConnect(authSuccess, authFailed); } }, 0); } }, } /** Module Registration */ this.NSGetFactory = cid => { this.NSGetFactory = XPCOMUtils.generateNSGetFactory([tbSyncDavCalendar]); return this.NSGetFactory(cid); }; DAV-4-TbSync-1.8/content/includes/tools.js000066400000000000000000001622041362346375200203330ustar00rootroot00000000000000/* * This file is part of DAV-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var tools = { getEmailsFromCard: function (aCard) { //return array of objects {meta, value} let emailData = []; try { emailData = JSON.parse(aCard.getProperty("X-DAV-JSON-Emails","[]").trim()); } catch (e) { //Components.utils.reportError(e); } // always use the core email field values, they could have been mod outside by the user, // not knowing that we store our stuff in X-DAV-JSON-Emails let emailFields = ["PrimaryEmail", "SecondEmail"]; for (let i = 0; i < emailFields.length; i++) { let email = aCard.getProperty(emailFields[i],"").trim(); if (email) { if (emailData.length > i) emailData[i].value = email; else emailData.push({value: email, meta: []}); } } return emailData; }, getPhoneNumbersFromCard: function (aCard) { //return array of objects {meta, value} let phones = aCard.getProperty("X-DAV-JSON-Phones","").trim(); try { if (phones) { return JSON.parse(phones); } } catch (e) { //Components.utils.reportError(e); } phones = []; //So this card is not a "DAV" card: Get the phone numbers from current numbers stored in //CellularNumber, FaxNumber, PagerNumber, WorkPhone, HomePhone"}, let todo = [ {field: "CellularNumber", meta: ["CELL"]}, {field: "FaxNumber", meta: ["FAX"]}, {field: "PagerNumber", meta: ["PAGER"]}, {field: "WorkPhone", meta: ["WORK"]}, {field: "HomePhone", meta: ["HOME"]} ]; for (let data of todo) { let phone = aCard.getProperty(data.field,"").trim(); if (phone) { phones.push({value: phone, meta: data.meta}); } } return phones; }, //* * * * * * * * * * * * * //* UTILS //* * * * * * * * * * * * * /** * Convert a byte array to a string - copied from lightning * * @param {octet[]} aResult The bytes to convert * @param {String} aCharset The character set of the bytes, defaults to utf-8 * @param {Boolean} aThrow If true, the function will raise an exception on error * @returns {?String} The string result, or null on error */ convertByteArray: function(aResult, aCharset="utf-8", aThrow) { try { return new TextDecoder(aCharset).decode(Uint8Array.from(aResult)); } catch (e) { if (aThrow) { throw e; } } return null; }, /** * Removes XML-invalid characters from a string. * @param {string} string - a string potentially containing XML-invalid characters, such as non-UTF8 characters, STX, EOX and so on. * @param {boolean} removeDiscouragedChars - a string potentially containing XML-invalid characters, such as non-UTF8 characters, STX, EOX and so on. * @returns : a sanitized string without all the XML-invalid characters. * * Source: https://www.ryadel.com/en/javascript-remove-xml-invalid-chars-characters-string-utf8-unicode-regex/ */ removeXMLInvalidChars: function (string, removeDiscouragedChars = true) { // remove everything forbidden by XML 1.0 specifications, plus the unicode replacement character U+FFFD var regex = /((?:[\0-\x08\x0B\f\x0E-\x1F\uFFFD\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]))/g; string = string.replace(regex, ""); if (removeDiscouragedChars) { // remove everything not suggested by XML 1.0 specifications regex = new RegExp( "([\\x7F-\\x84]|[\\x86-\\x9F]|[\\uFDD0-\\uFDEF]|(?:\\uD83F[\\uDFFE\\uDFFF])|(?:\\uD87F[\\uDF"+ "FE\\uDFFF])|(?:\\uD8BF[\\uDFFE\\uDFFF])|(?:\\uD8FF[\\uDFFE\\uDFFF])|(?:\\uD93F[\\uDFFE\\uD"+ "FFF])|(?:\\uD97F[\\uDFFE\\uDFFF])|(?:\\uD9BF[\\uDFFE\\uDFFF])|(?:\\uD9FF[\\uDFFE\\uDFFF])"+ "|(?:\\uDA3F[\\uDFFE\\uDFFF])|(?:\\uDA7F[\\uDFFE\\uDFFF])|(?:\\uDABF[\\uDFFE\\uDFFF])|(?:\\"+ "uDAFF[\\uDFFE\\uDFFF])|(?:\\uDB3F[\\uDFFE\\uDFFF])|(?:\\uDB7F[\\uDFFE\\uDFFF])|(?:\\uDBBF"+ "[\\uDFFE\\uDFFF])|(?:\\uDBFF[\\uDFFE\\uDFFF])(?:[\\0-\\t\\x0B\\f\\x0E-\\u2027\\u202A-\\uD7FF\\"+ "uE000-\\uFFFF]|[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|"+ "(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF]))", "g"); string = string.replace(regex, ""); } return string; }, xmlns: function (ns) { let _xmlns = []; for (let i=0; i < ns.length; i++) { _xmlns.push('xmlns:'+ns[i]+'="'+dav.sync.ns[ns[i]]+'"'); } return _xmlns.join(" "); }, parseUri: function (aUri) { let uri; try { // Test if the entered uri can be parsed. uri = Services.io.newURI(aUri, null, null); } catch (ex) { throw new Error("invalid-calendar-url"); } return uri; }, parseVcardDateTime: function ( newServerValue, metadata ) { if (!newServerValue) { return false; } /* ** This accepts RFC2426 BDAY values (with/without hyphens), ** though TB doesn't handle the time part of date-times, so we discard it. */ let bday = newServerValue.match( /^(\d{4})-?(\d{2})-?(\d{2})/ ); if (!bday) { return false; } /* ** Apple Contacts shoehorns date with missing year into vcard3 thus: BDAY;X-APPLE-OMIT-YEAR=1604:1604-03-15 ** Later in vcard4, it will be represented as BDAY:--0315 */ if (metadata && metadata['x-apple-omit-year'] && metadata['x-apple-omit-year'] == bday[1]) { bday[1] = ''; } return bday; }, getEmailsFromJSON: function (emailDataJSON) { let emailFields = {}; if (emailDataJSON) { try { // We pack the first entry into PrimaryEmail and the second one into SecondEmail. // For compatibility with the Phones, we return arrays, even though we only return // one element per array. let emailData = JSON.parse(emailDataJSON); emailFields = {PrimaryEmail:[], SecondEmail:[]}; for (let d=0; d < emailData.length && d < 2; d++) { let field = (d==0) ? "PrimaryEmail" : "SecondEmail"; emailFields[field].push(emailData[d].value); } } catch(e) { //something went wrong Components.utils.reportError(e); } } //object with TB field names as keys and array of emails as values return emailFields; }, getPhoneNumbersFromJSON: function (phoneDataJSON) { let phoneFields = {}; if (phoneDataJSON) { try { //we first search and remove CELL, FAX, PAGER and WORK from the list and put the remains into HOME let phoneData = JSON.parse(phoneDataJSON); let phoneMap = [ {meta: "CELL", field: "CellularNumber"}, {meta: "FAX", field: "FaxNumber"}, {meta: "PAGER", field: "PagerNumber"}, {meta: "WORK", field: "WorkPhone"}, {meta: "", field: "HomePhone"}, ]; for (let m=0; m < phoneMap.length; m++) { phoneFields[phoneMap[m].field] = []; for (let d=phoneData.length-1; d >= 0; d--) { if (phoneData[d].meta.includes(phoneMap[m].meta) || phoneMap[m].meta == "") { phoneFields[phoneMap[m].field].unshift(phoneData[d].value); phoneData.splice(d,1); } } } } catch(e) { //something went wrong Components.utils.reportError(e); } } //object with TB field names as keys and array of numbers as values return phoneFields; }, //* * * * * * * * * * * * * * //* EVALUATE XML RESPONSES * //* * * * * * * * * * * * * * convertToXML: function(text) { //try to convert response body to xml let xml = null; let oParser = new DOMParser(); try { xml = oParser.parseFromString(dav.tools.removeXMLInvalidChars(text), "application/xml"); } catch (e) { //however, domparser does not throw an error, it returns an error document //https://developer.mozilla.org/de/docs/Web/API/DOMParser xml = null; } //check if xml is error document if (xml && xml.documentElement.nodeName == "parsererror") { xml = null; } return xml; }, evaluateNode: function (_node, path) { let node = _node; let valid = false; for (let i=0; i < path.length; i++) { let children = node.children; valid = false; for (let c=0; c < children.length; c++) { if (children[c].localName == path[i][1] && children[c].namespaceURI == dav.sync.ns[path[i][0]]) { node = children[c]; valid = true; break; } } if (!valid) { //none of the children matched the path abort return null; } } if (valid) return node; return null; }, hrefMatch:function (_requestHref, _responseHref) { if (_requestHref === null) return true; let requestHref = _requestHref; let responseHref = _responseHref; while (requestHref.endsWith("/")) { requestHref = requestHref.slice(0,-1); } while (responseHref.endsWith("/")) { responseHref = responseHref.slice(0,-1); } if (requestHref.endsWith(responseHref) || decodeURIComponent(requestHref).endsWith(responseHref) || requestHref.endsWith(decodeURIComponent(responseHref))) return true; return false; }, getNodeTextContentFromMultiResponse: function (response, path, href = null, status = ["200"]) { for (let i=0; i < response.multi.length; i++) { let node = dav.tools.evaluateNode(response.multi[i].node, path); if (node !== null && dav.tools.hrefMatch(href, response.multi[i].href) && status.includes(response.multi[i].status)) { return node.textContent; } } return null; }, getNodesTextContentFromMultiResponse: function (response, path, href = null, status = "200") { //remove last element from path let lastPathElement = path.pop(); let rv = []; for (let i=0; i < response.multi.length; i++) { let node = dav.tools.evaluateNode(response.multi[i].node, path); if (node !== null && dav.tools.hrefMatch(href, response.multi[i].href) && response.multi[i].status == status) { //get all children let children = node.getElementsByTagNameNS(dav.sync.ns[lastPathElement[0]], lastPathElement[1]); for (let c=0; c < children.length; c++) { if (children[c].textContent) rv.push(children[c].textContent); } } } return rv; }, getMultiGetRequest: function(hrefs) { let request = ""; let counts = 0; for (let i=0; i < hrefs.length; i++) { request += ""+hrefs[i]+""; counts++; } request += ""; if (counts > 0) return request; else return null; }, //* * * * * * * * * * * //* CARDS OPERATIONS * //* * * * * * * * * * * addContact: async function(syncData, id, data, etag) { let vCard = data.textContent.trim(); let vCardData = dav.vCard.parse(vCard); //check if contact or mailinglist if (!dav.tools.vCardIsMailingList (syncData, id, null, vCard, vCardData, etag)) { //prepare new contact card let card = syncData.target.createNewCard(); card.setProperty("X-DAV-HREF", id); card.setProperty("X-DAV-ETAG", etag.textContent); card.setProperty("X-DAV-VCARD", vCard); await dav.tools.setThunderbirdCardFromVCard(syncData, card, vCardData); syncData.target.addItem(card); } }, modifyContact: async function(syncData, id, data, etag) { let vCard = data.textContent.trim(); let vCardData = dav.vCard.parse(vCard); //get card let card = syncData.target.getItemFromProperty("X-DAV-HREF", id); if (card) { //check if contact or mailinglist to update card if (!dav.tools.vCardIsMailingList (syncData, id, card, vCard, vCardData, etag)) { //get original vCard data as stored by last update from server let oCard = card.getProperty("X-DAV-VCARD"); let oCardData = oCard ? dav.vCard.parse(oCard) : null; card.setProperty("X-DAV-ETAG", etag.textContent); card.setProperty("X-DAV-VCARD", vCard); await dav.tools.setThunderbirdCardFromVCard(syncData, card, vCardData, oCardData); syncData.target.modifyItem(card); } } else { //card does not exists, create it? } }, //check if vCard is a mailinglist and handle it vCardIsMailingList: function (syncData, id, _list, vCard, vCardData, etag) { if (vCardData.hasOwnProperty("X-ADDRESSBOOKSERVER-KIND") && vCardData["X-ADDRESSBOOKSERVER-KIND"][0].value == "group") { if (!syncData.accountData.getAccountProperty("syncGroups")) { //user did not enable group sync, so do nothing, but return true so this card does not get added as a real card return true; } let vCardInfo = dav.tools.getGroupInfoFromCardData(vCardData, syncData.target, false); //just get the name, not the members //if no card provided, create a new one let list = _list; if (!list) { list = syncData.target.createNewList(); list.setProperty("X-DAV-HREF", id); list.setProperty("X-DAV-UID", vCardInfo.uid); list.setProperty("ListName", vCardInfo.name); syncData.target.addItem(list); } else { list.setProperty("ListName", vCardInfo.name); syncData.target.modifyItem(list); } //get original vCardData from last server contact, needed for "smart merge" on changes on both sides let oCardData = dav.vCard.parse(list.getProperty("X-DAV-VCARD")); //store all old and new vCards for later processing (cannot do it here, because it is not guaranteed, that all members exists already) syncData.foundMailingListsDuringDownSync[id] = {oCardData, vCardData}; //update properties list.setProperty("X-DAV-ETAG", etag.textContent); list.setProperty("X-DAV-VCARD", vCard); // AbItem implementation: Custom properties of lists are updated instantly, no need to call target.modifyItem(list); return true; } else { return false; } }, //* * * * * * * * * * * //* ACTUAL SYNC MAGIC * //* * * * * * * * * * * //helper function: extract the associated meta.type of an entry getItemMetaType: function (vCardData, item, i, typefield) { if (vCardData[item][i].meta && vCardData[item][i].meta[typefield] && vCardData[item][i].meta[typefield].length > 0) { //vCard parser now spilts up meta types into single array values //TYPE="home,cell" and TYPE=home;Type=cell will be received as ["home", "cell"] return vCardData[item][i].meta[typefield]; } return []; }, //helper function: for each entry for the given item, extract the associated meta.type getMetaTypeData: function (vCardData, item, typefield) { let metaTypeData = []; for (let i=0; i < vCardData[item].length; i++) { metaTypeData.push( dav.tools.getItemMetaType(vCardData, item, i, typefield) ); } return metaTypeData; }, fixArrayValue: function (vCardData, vCardField, index) { if (!Array.isArray(vCardData[vCardField.item][vCardField.entry].value)) { let v = vCardData[vCardField.item][vCardField.entry].value; vCardData[vCardField.item][vCardField.entry].value = [v]; } while (vCardData[vCardField.item][vCardField.entry].value.length < index) vCardData[vCardField.item][vCardField.entry].value.push(""); }, getSaveArrayValue: function (vCardValue, index) { if (Array.isArray(vCardValue)) { if(vCardValue.length > index) return vCardValue[index]; else return ""; } else if (index == 0) return vCardValue; else return ""; }, supportedProperties: [ {name: "DisplayName", minversion: "0.4"}, {name: "FirstName", minversion: "0.4"}, {name: "X-DAV-PrefixName", minversion: "0.12.13"}, {name: "X-DAV-MiddleName", minversion: "0.8.8"}, {name: "X-DAV-SuffixName", minversion: "0.12.13"}, {name: "X-DAV-UID", minversion: "0.10.36"}, {name: "X-DAV-JSON-Phones", minversion: "0.4"}, {name: "X-DAV-JSON-Emails", minversion: "0.4"}, {name: "LastName", minversion: "0.4"}, {name: "NickName", minversion: "0.4"}, {name: "Birthday", minversion: "0.4"}, //fake, will trigger special handling {name: "Photo", minversion: "0.4"}, //fake, will trigger special handling {name: "HomeCity", minversion: "0.4"}, {name: "HomeCountry", minversion: "0.4"}, {name: "HomeZipCode", minversion: "0.4"}, {name: "HomeState", minversion: "0.4"}, {name: "HomeAddress", minversion: "0.4"}, {name: "HomeAddress2", minversion: "1.4.1"}, {name: "WorkCity", minversion: "0.4"}, {name: "WorkCountry", minversion: "0.4"}, {name: "WorkZipCode", minversion: "0.4"}, {name: "WorkState", minversion: "0.4"}, {name: "WorkAddress", minversion: "0.4"}, {name: "WorkAddress2", minversion: "1.4.1"}, {name: "Categories", minversion: "0.4"}, {name: "JobTitle", minversion: "0.4"}, {name: "Department", minversion: "0.4"}, {name: "Company", minversion: "0.4"}, {name: "WebPage1", minversion: "0.4"}, {name: "WebPage2", minversion: "0.4"}, {name: "Notes", minversion: "0.4"}, {name: "PreferMailFormat", minversion: "0.4"}, {name: "Custom1", minversion: "0.4"}, {name: "Custom2", minversion: "0.4"}, {name: "Custom3", minversion: "0.4"}, {name: "Custom4", minversion: "0.4"}, {name: "_GoogleTalk", minversion: "0.4"}, {name: "_JabberId", minversion: "0.4"}, {name: "_Yahoo", minversion: "0.4"}, {name: "_QQ", minversion: "0.4"}, {name: "_AimScreenName", minversion: "0.4"}, {name: "_MSN", minversion: "0.4"}, {name: "_Skype", minversion: "0.4"}, {name: "_ICQ", minversion: "0.4"}, {name: "_IRC", minversion: "0.4"}, ], //map thunderbird fields to simple vcard fields without additional types simpleMap : { "X-DAV-UID" : "uid", "Birthday" : "bday", //fake "Photo" : "photo", //fake "JobTitle" : "title", "Department" : "org", "Company" : "org", "DisplayName" : "fn", "NickName" : "nickname", "Categories" : "categories", "Notes" : "note", "FirstName" : "n", "X-DAV-PrefixName" : "n", "X-DAV-MiddleName" : "n", "X-DAV-SuffixName" : "n", "LastName" : "n", "PreferMailFormat" : "X-MOZILLA-HTML", "Custom1" : "X-MOZILLA-CUSTOM1", "Custom2" : "X-MOZILLA-CUSTOM2", "Custom3" : "X-MOZILLA-CUSTOM3", "Custom4" : "X-MOZILLA-CUSTOM4", }, //map thunderbird fields to vcard fields with additional types complexMap : { "WebPage1" : {item: "url", type: "WORK"}, "WebPage2" : {item: "url", type: "HOME"}, "HomeCity" : {item: "adr", type: "HOME"}, "HomeCountry" : {item: "adr", type: "HOME"}, "HomeZipCode" : {item: "adr", type: "HOME"}, "HomeState" : {item: "adr", type: "HOME"}, "HomeAddress" : {item: "adr", type: "HOME"}, "HomeAddress2" : {item: "adr", type: "HOME"}, "WorkCity" : {item: "adr", type: "WORK"}, "WorkCountry" : {item: "adr", type: "WORK"}, "WorkZipCode" : {item: "adr", type: "WORK"}, "WorkState" : {item: "adr", type: "WORK"}, "WorkAddress" : {item: "adr", type: "WORK"}, "WorkAddress2" : {item: "adr", type: "WORK"}, }, //map thunderbird fields to impp vcard fields with additional x-service-types imppMap : { "_GoogleTalk" : {item: "impp" , prefix: "xmpp:", type: "GOOGLETALK"}, //actually x-service-type "_JabberId" : {item: "impp", prefix: "xmpp:", type: "JABBER"}, "_Yahoo" : {item: "impp", prefix: "ymsgr:", type: "YAHOO"}, "_QQ" : {item: "impp", prefix: "x-apple:", type: "QQ"}, "_AimScreenName" : {item: "impp", prefix: "aim:", type: "AIM"}, "_MSN" : {item: "impp", prefix: "msnim:", type: "MSN"}, "_Skype" : {item: "impp", prefix: "skype:", type: "SKYPE"}, "_ICQ" : {item: "impp", prefix: "aim:", type: "ICQ"}, "_IRC" : {item: "impp", prefix: "irc:", type: "IRC"}, }, //For a given Thunderbird property, identify the vCard field // -> which main item // -> which array element (based on metatype, if needed) //https://tools.ietf.org/html/rfc2426#section-3.6.1 getVCardField: function (syncData, property, vCardData) { let data = {item: "", metatype: [], metatypefield: "type", entry: -1, prefix: ""}; if (vCardData) { //handle special cases independently, those from *Map will be handled by default switch (property) { case "X-DAV-JSON-Emails": { data.metatype.push("OTHER"); //default for new entries data.item = "email"; if (vCardData[data.item] && vCardData[data.item].length > 0) { //NOOP, just return something, if present data.entry = 0; } } break; case "X-DAV-JSON-Phones": { data.metatype.push("VOICE"); //default for new entries data.item = "tel"; if (vCardData[data.item] && vCardData[data.item].length > 0) { //NOOP, just return something, if present data.entry = 0; } } break; default: //Check *Maps if (dav.tools.simpleMap.hasOwnProperty(property)) { data.item = dav.tools.simpleMap[property]; if (vCardData[data.item] && vCardData[data.item].length > 0) data.entry = 0; } else if (dav.tools.imppMap.hasOwnProperty(property)) { let type = dav.tools.imppMap[property].type; data.metatype.push(type); data.item = dav.tools.imppMap[property].item; data.prefix = dav.tools.imppMap[property].prefix; data.metatypefield = "x-service-type"; if (vCardData[data.item]) { let metaTypeData = dav.tools.getMetaTypeData(vCardData, data.item, data.metatypefield); let valids = []; for (let i=0; i < metaTypeData.length; i++) { if (metaTypeData[i].includes(type)) valids.push(i); } if (valids.length > 0) data.entry = valids[0]; } } else if (dav.tools.complexMap.hasOwnProperty(property)) { let type = dav.tools.complexMap[property].type; let invalidTypes = (dav.tools.complexMap[property].invalidTypes) ? dav.tools.complexMap[property].invalidTypes : []; data.metatype.push(type); data.item = dav.tools.complexMap[property].item; if (vCardData[data.item]) { let metaTypeData = dav.tools.getMetaTypeData(vCardData, data.item, data.metatypefield); let valids = []; for (let i=0; i < metaTypeData.length; i++) { //check if this includes the requested type and also none of the invalid types if (metaTypeData[i].includes(type) && metaTypeData[i].filter(value => -1 !== invalidTypes.indexOf(value)).length == 0) valids.push(i); } if (valids.length > 0) data.entry = valids[0]; } } else throw "Unknown TB property <"+property+">"; } } return data; }, //turn the given vCardValue into a string to be stored as a Thunderbird property getThunderbirdPropertyValueFromVCard: function (syncData, property, vCardData, vCardField) { let vCardValue = (vCardData && vCardField && vCardField.entry != -1 && vCardData[vCardField.item] && vCardData[vCardField.item][vCardField.entry] && vCardData[vCardField.item][vCardField.entry].value) ? vCardData[vCardField.item][vCardField.entry].value : null; if (vCardValue === null) { return null; } //handle all special fields, which are not plain strings switch (property) { case "HomeCity": case "HomeCountry": case "HomeZipCode": case "HomeState": case "HomeAddress": case "HomeAddress2": case "WorkCity": case "WorkCountry": case "WorkZipCode": case "WorkState": case "WorkAddress": case "WorkAddress2": { let field = property.substring(4); let adr = (Services.vc.compare("0.8.11", syncData.currentFolderData.getFolderProperty("createdWithProviderVersion")) > 0) ? ["OfficeBox","Address2","Address","City","Country","ZipCode", "State"] //WRONG : ["OfficeBox","Address2","Address","City","State","ZipCode", "Country"]; //RIGHT, fixed in 0.8.11 let index = adr.indexOf(field); return dav.tools.getSaveArrayValue(vCardValue, index); } break; case "FirstName": case "LastName": case "X-DAV-PrefixName": case "X-DAV-MiddleName": case "X-DAV-SuffixName": { let index = ["LastName","FirstName","X-DAV-MiddleName","X-DAV-PrefixName","X-DAV-SuffixName"].indexOf(property); return dav.tools.getSaveArrayValue(vCardValue, index); } break; case "Department": case "Company": { let index = ["Company","Department"].indexOf(property); return dav.tools.getSaveArrayValue(vCardValue, index); } break; case "Categories": return (Array.isArray(vCardValue) ? vCardValue.join("\u001A") : vCardValue); break; case "PreferMailFormat": if (vCardValue.toLowerCase() == "true") return 2; if (vCardValue.toLowerCase() == "false") return 1; return 0; break; case "X-DAV-JSON-Phones": case "X-DAV-JSON-Emails": { //this is special, we need to return the full JSON object let entries = []; let metaTypeData = dav.tools.getMetaTypeData(vCardData, vCardField.item, vCardField.metatypefield); for (let i=0; i < metaTypeData.length; i++) { let entry = {}; entry.meta = metaTypeData[i]; entry.value = vCardData[vCardField.item][i].value; entries.push(entry); } return JSON.stringify(entries); } break; default: { //should be a single string let v = (Array.isArray(vCardValue)) ? vCardValue.join(" ") : vCardValue; if (vCardField.prefix.length > 0 && v.startsWith(vCardField.prefix)) return v.substring(vCardField.prefix.length); else return v; } } }, //add/update the given Thunderbird propeties value in vCardData obj updateValueOfVCard: function (syncData, property, vCardData, vCardField, value) { let add = false; let store = value ? true : false; let remove = (!store && vCardData[vCardField.item] && vCardField.entry != -1); //preperations if this item does not exist if (store && vCardField.entry == -1) { //entry does not exists, does the item exists? if (!vCardData[vCardField.item]) vCardData[vCardField.item] = []; let newItem = {}; if (vCardField.metatype.length > 0) { newItem["meta"] = {}; newItem["meta"][vCardField.metatypefield] = vCardField.metatype; } vCardField.entry = vCardData[vCardField.item].push(newItem) - 1; add = true; } //handle all special fields, which are not plain strings switch (property) { case "HomeCity": case "HomeCountry": case "HomeZipCode": case "HomeState": case "HomeAddress": case "HomeAddress2": case "WorkCity": case "WorkCountry": case "WorkZipCode": case "WorkState": case "WorkAddress": case "WorkAddress2": { let field = property.substring(4); let adr = (Services.vc.compare("0.8.11", syncData.currentFolderData.getFolderProperty("createdWithProviderVersion")) > 0) ? ["OfficeBox","Address2","Address","City","Country","ZipCode", "State"] //WRONG : ["OfficeBox","Address2","Address","City","State","ZipCode", "Country"]; //RIGHT, fixed in 0.8.11 let index = adr.indexOf(field); if (store) { if (add) vCardData[vCardField.item][vCardField.entry].value = ["","","","","","",""]; dav.tools.fixArrayValue(vCardData, vCardField, index); vCardData[vCardField.item][vCardField.entry].value[index] = value; } else if (remove) { dav.tools.fixArrayValue(vCardData, vCardField, index); vCardData[vCardField.item][vCardField.entry].value[index] = ""; //Will be completly removed by the parser, if all fields are empty! } } break; case "FirstName": case "X-DAV-PrefixName": case "X-DAV-MiddleName": case "X-DAV-SuffixName": case "LastName": { let index = ["LastName","FirstName","X-DAV-MiddleName","X-DAV-PrefixName","X-DAV-SuffixName"].indexOf(property); if (store) { if (add) vCardData[vCardField.item][vCardField.entry].value = ["","","","",""]; dav.tools.fixArrayValue(vCardData, vCardField, index); vCardData[vCardField.item][vCardField.entry].value[index] = value; } else if (remove) { dav.tools.fixArrayValue(vCardData, vCardField, index); vCardData[vCardField.item][vCardField.entry].value[index] = ""; //Will be completly removed by the parser, if all fields are empty! } } break; case "Department": case "Company": { let index = ["Company","Department"].indexOf(property); if (store) { if (add) vCardData[vCardField.item][vCardField.entry].value = ["",""]; dav.tools.fixArrayValue(vCardData, vCardField, index); vCardData[vCardField.item][vCardField.entry].value[index] = value; } else if (remove && vCardData[vCardField.item][vCardField.entry].value.length > index) { dav.tools.fixArrayValue(vCardData, vCardField, index); vCardData[vCardField.item][vCardField.entry].value[index] = ""; //Will be completly removed by the parser, if all fields are empty! } } break; case "Categories": if (store) vCardData[vCardField.item][vCardField.entry].value = value.split("\u001A"); else if (remove) vCardData[vCardField.item][vCardField.entry].value = []; break; case "PreferMailFormat": { if (store) { let v = (value == 2) ? "TRUE" : (value == 1) ? "FALSE" : ""; vCardData[vCardField.item][vCardField.entry].value = v; } else if (remove) vCardData[vCardField.item][vCardField.entry].value = ""; } break; case "Emails": //also update meta case "Phones": //also update meta if (store) { vCardData[vCardField.item][vCardField.entry].value = vCardField.prefix + value; if (!vCardData[vCardField.item][vCardField.entry].hasOwnProperty("meta")) { vCardData[vCardField.item][vCardField.entry].meta = {}; } vCardData[vCardField.item][vCardField.entry].meta[vCardField.metatypefield] = vCardField.metatype; } else if (remove) vCardData[vCardField.item][vCardField.entry].value = ""; break; default: //should be a string if (store) vCardData[vCardField.item][vCardField.entry].value = vCardField.prefix + value; else if (remove) vCardData[vCardField.item][vCardField.entry].value = ""; } }, //MAIN FUNCTIONS FOR UP/DOWN SYNC //update send from server to client setThunderbirdCardFromVCard: async function(syncData, card, vCardData, oCardData = null) { if (TbSync.prefs.getIntPref("log.userdatalevel") > 2) TbSync.dump("JSON from vCard", JSON.stringify(vCardData)); //if (oCardData) TbSync.dump("JSON from oCard", JSON.stringify(oCardData)); for (let f=0; f < dav.tools.supportedProperties.length; f++) { //Skip sync fields that have been added after this folder was created (otherwise we would delete them) if (Services.vc.compare(dav.tools.supportedProperties[f].minversion, syncData.currentFolderData.getFolderProperty("createdWithProviderVersion"))> 0) continue; let property = dav.tools.supportedProperties[f].name; let vCardField = dav.tools.getVCardField(syncData, property, vCardData); let newServerValue = dav.tools.getThunderbirdPropertyValueFromVCard(syncData, property, vCardData, vCardField); let oCardField = dav.tools.getVCardField(syncData, property, oCardData); let oldServerValue = dav.tools.getThunderbirdPropertyValueFromVCard(syncData, property, oCardData, oCardField); //smart merge: only update the property, if it has changed on the server (keep local modifications) if (newServerValue !== oldServerValue) { //some "properties" need special handling switch (property) { case "Photo": { if (newServerValue) { let type = ""; try { type = vCardData[vCardField.item][0].meta.type[0].toLowerCase(); } catch (e) { Components.utils.reportError(e); } // check for inline data or linked data if (vCardData[vCardField.item][0].meta && vCardData[vCardField.item][0].meta.encoding) { card.addPhoto(TbSync.generateUUID(), vCardData[vCardField.item][0].value, type || "jpg"); } else if (vCardData[vCardField.item][0].meta && Array.isArray(vCardData[vCardField.item][0].meta.value) && vCardData[vCardField.item][0].meta.value[0].toString().toLowerCase() == "uri") { let connectionData = new dav.network.ConnectionData(); connectionData.eventLogInfo = syncData.connectionData.eventLogInfo; // add credentials, if image is on the account server, go anonymous otherwise try { if (vCardData[vCardField.item][0].value.split("://").pop().startsWith(syncData.connectionData.fqdn)) { connectionData.password = syncData.connectionData.password; connectionData.username = syncData.connectionData.username; } let rv = await dav.network.sendRequest("", vCardData[vCardField.item][0].value , "GET", connectionData, {}, {responseType: "base64"}); card.addPhoto(TbSync.generateUUID(), rv, type || this.getImageExtension(vCardData[vCardField.item][0].value), vCardData[vCardField.item][0].value); } catch(e) { TbSync.eventlog.add("warning", syncData.eventLogInfo,"Could not extract externally linked photo from vCard", JSON.stringify(vCardData)); } } } else { //clear card.deleteProperty("PhotoName"); card.deleteProperty("PhotoType"); card.deleteProperty("PhotoURI"); } } break; case "Birthday": { if ( newServerValue ) { let bday = dav.tools.parseVcardDateTime( newServerValue, vCardData[vCardField.item][0].meta ); card.setProperty("BirthYear", bday[1]); card.setProperty("BirthMonth", bday[2]); card.setProperty("BirthDay", bday[3]); } else { card.deleteProperty("BirthYear"); card.deleteProperty("BirthMonth"); card.deleteProperty("BirthDay"); } } break; case "X-DAV-JSON-Emails": case "X-DAV-JSON-Phones": { //This field contains all the JSON encoded values and TbSync has its own UI to display them. //However, we also want to fill the standard TB fields. let tbData; switch (property) { case "X-DAV-JSON-Emails" : tbData = dav.tools.getEmailsFromJSON(newServerValue); break; case "X-DAV-JSON-Phones" : tbData = dav.tools.getPhoneNumbersFromJSON(newServerValue); break; } for (let field in tbData) { if (tbData.hasOwnProperty(field)) { //set or delete TB Property if ( tbData[field].length > 0 ) { card.setProperty(field, tbData[field].join(", ")); } else { card.deleteProperty(field); } } } } default: { if (newServerValue) { //set card.setProperty(property, newServerValue); } else { //clear (del if possible) card.setProperty(property, ""); try { card.deleteProperty(property); } catch (e) {} } } break; } } } }, getGroupInfoFromCardData: function (vCardData, addressBook, getMembers = true) { let members = []; let name = "Unlabled Group"; try { name = vCardData["fn"][0].value; } catch (e) {} let uid = ""; try { uid = vCardData["uid"][0].value; } catch (e) {} if (getMembers && vCardData.hasOwnProperty("X-ADDRESSBOOKSERVER-MEMBER")) { for (let i=0; i < vCardData["X-ADDRESSBOOKSERVER-MEMBER"].length; i++) { let member = vCardData["X-ADDRESSBOOKSERVER-MEMBER"][i].value.replace(/^(urn:uuid:)/,""); // "member" is the X-DAV-UID property of the member vCard members.push(member); } } return {members, name, uid}; }, //build group card getVCardFromThunderbirdListCard: function(syncData, list, generateUID = false) { let currentCard = list.getProperty("X-DAV-VCARD").trim(); let cCardData = dav.vCard.parse(currentCard); let vCardData = dav.vCard.parse(currentCard); let members = list.getMembersPropertyList("X-DAV-UID"); if (!vCardData.hasOwnProperty("version")) vCardData["version"] = [{"value": "3.0"}]; let listName = list.getProperty("ListName", "Unlabled List"); vCardData["fn"] = [{"value": listName}]; vCardData["n"] = [{"value": listName}]; vCardData["X-ADDRESSBOOKSERVER-KIND"] = [{"value": "group"}]; // check UID status let uidProp = list.getProperty("X-DAV-UID"); let uidItem = ""; try { uidItem = vCardData["uid"][0].value; } catch (e) {} if (!uidItem && !uidProp) { TbSync.eventlog.add("info", syncData.eventLogInfo, "Generated missing UID for list <"+listName+">"); let uid = TbSync.generateUUID(); list.setProperty("X-DAV-UID", uid); vCardData["uid"] = [{"value": uid}]; } else if (!uidItem && uidProp) { vCardData["uid"] = [{"value": uidProp}]; TbSync.eventlog.add("info", syncData.eventLogInfo, "Updating item uid from uid property for list <"+listName+">", JSON.stringify({uidProp, uidItem})); } else if (uidItem && !uidProp) { list.setProperty("X-DAV-UID", uidItem); TbSync.eventlog.add("info", syncData.eventLogInfo, "Updating uid property from item uid of list <"+listName+">", JSON.stringify({uidProp, uidItem})); } else if (uidItem != uidProp) { list.setProperty("X-DAV-UID", uidItem); TbSync.eventlog.add("info", syncData.eventLogInfo, "Updating uid property from item uid of list <"+listName+">", JSON.stringify({uidProp, uidItem})); } //build memberlist from scratch vCardData["X-ADDRESSBOOKSERVER-MEMBER"]=[]; for (let member of members) { // member has the UID (X-DAV-UID) of each member vCardData["X-ADDRESSBOOKSERVER-MEMBER"].push({"value": "urn:uuid:" + member}); } let newCard = dav.vCard.generate(vCardData).trim(); let oldCard = dav.vCard.generate(cCardData).trim(); let modified = false; if (oldCard != newCard) { TbSync.dump("List has been modified!",""); TbSync.dump("currentCard", oldCard); TbSync.dump("newCard", newCard); modified = true; } return {data: newCard, etag: list.getProperty("X-DAV-ETAG"), modified: modified}; }, setDefaultMetaButKeepCaseIfPresent: function(defaults, currentObj) { const keys = Object.keys(defaults); for (const key of keys) { let defaultValue = defaults[key]; // we need to set this value, but do not want to cause a "modified" if it was set like this before, but just with different case // so keep the current case try { let c = currentObj.meta[key][0]; if (c.toLowerCase() == defaultValue.toLowerCase()) defaultValue = c; } catch(e) { //Components.utils.reportError(e); } if (!currentObj.hasOwnProperty("meta")) currentObj.meta = {}; currentObj.meta[key]=[defaultValue]; } }, getImageExtension: function(filename) { // get extension from filename let extension = ""; try { extension = filename.toString().split(".").pop().toUpperCase(); } catch (e) {} return extension; }, //return the stored vcard of the card (or empty vcard if none stored) and merge local changes getVCardFromThunderbirdContactCard: function(syncData, card, generateUID = false) { let currentCard = card.getProperty("X-DAV-VCARD").trim(); let cCardData = dav.vCard.parse(currentCard); let vCardData = dav.vCard.parse(currentCard); for (let f=0; f < dav.tools.supportedProperties.length; f++) { //Skip sync fields that have been added after this folder was created (otherwise we would delete them) if (Services.vc.compare(dav.tools.supportedProperties[f].minversion, syncData.currentFolderData.getFolderProperty("createdWithProviderVersion"))> 0) continue; let property = dav.tools.supportedProperties[f].name; let vCardField = dav.tools.getVCardField(syncData, property, vCardData); //some "properties" need special handling switch (property) { case "Photo": { let extension = this.getImageExtension(card.getProperty("PhotoURI", "")); let type = (extension == "JPG") ? "JPEG" : extension; if (card.getProperty("PhotoType", "") == "file") { TbSync.eventlog.add("info", syncData.eventLogInfo, "before photo ("+vCardField.item+")", JSON.stringify(vCardData)); dav.tools.updateValueOfVCard(syncData, property, vCardData, vCardField, card.getPhoto()); this.setDefaultMetaButKeepCaseIfPresent({encoding : "B", type : type}, vCardData[vCardField.item][0]); TbSync.eventlog.add("info", syncData.eventLogInfo, "after photo ("+vCardField.item+")", JSON.stringify(vCardData)); } else if (card.getProperty("PhotoType", "") == "web" && card.getProperty("PhotoURI", "")) { TbSync.eventlog.add("info", syncData.eventLogInfo, "before photo ("+vCardField.item+")", JSON.stringify(vCardData)); dav.tools.updateValueOfVCard(syncData, property, vCardData, vCardField, card.getProperty("PhotoURI", "")); this.setDefaultMetaButKeepCaseIfPresent({value : "uri", type : type}, vCardData[vCardField.item][0]); TbSync.eventlog.add("info", syncData.eventLogInfo, "after photo ("+vCardField.item+")", JSON.stringify(vCardData)); } } break; case "Birthday": { // Support missing year in vcard3, as done by Apple Contacts. const APPLE_MISSING_YEAR_MARK = "1604"; let birthYear = parseInt(card.getProperty("BirthYear", 0)); let birthMonth = parseInt(card.getProperty("BirthMonth", 0)); let birthDay = parseInt(card.getProperty("BirthDay", 0)); if (!birthYear) { birthYear = APPLE_MISSING_YEAR_MARK; } let value = ""; if (birthYear && birthMonth && birthDay) { // TODO: for vcard4, we'll need to get rid of the hyphens and support missing date elements value = birthYear + "-" + ("00"+birthMonth).slice(-2) + "-" + ("00"+birthDay).slice(-2); } dav.tools.updateValueOfVCard(syncData, property, vCardData, vCardField, value); if (birthYear == APPLE_MISSING_YEAR_MARK && Array.isArray(vCardData[vCardField.item]) && vCardData[vCardField.item].length > 0) { vCardData[vCardField.item][0].meta = {"x-apple-omit-year": [APPLE_MISSING_YEAR_MARK]}; } } break; case "X-DAV-JSON-Emails": { //this gets us all emails let emails = dav.tools.getEmailsFromCard(card); let idx = 0; //store default meta type let defaultMeta = vCardField.metatype; for (let i=0; i < emails.length || (vCardData.hasOwnProperty(vCardField.item) && idx < vCardData[vCardField.item].length); i++) { //get value or or empty if entry is to be deleted let value = (i < emails.length) ? emails[i].value : ""; //fix for bug 1522453 - ignore these if (value.endsWith("@bug1522453")) continue; //do we have a meta type? otherwise stick to default if (i < emails.length && emails[i].meta.length > 0) { vCardField.metatype = emails[i].meta; } else { vCardField.metatype = defaultMeta; } //remove: value == "" and index != -1 //add value != "" and index == -1 vCardField.entry = idx++; if (!(vCardData.hasOwnProperty(vCardField.item) && vCardField.entry < vCardData[vCardField.item].length)) vCardField.entry = -1; //need to add a new one dav.tools.updateValueOfVCard(syncData, "Emails", vCardData, vCardField, value); } } break; case "X-DAV-JSON-Phones": { //this gets us all phones let phones = dav.tools.getPhoneNumbersFromCard(card); let idx = 0; //store default meta type let defaultMeta = vCardField.metatype; for (let i=0; i < phones.length || (vCardData.hasOwnProperty(vCardField.item) && idx < vCardData[vCardField.item].length); i++) { //get value or or empty if entry is to be deleted let value = (i < phones.length) ? phones[i].value : ""; //do we have a meta type? otherwise stick to default if (i < phones.length && phones[i].meta.length > 0) { vCardField.metatype = phones[i].meta; } else { vCardField.metatype = defaultMeta; } //remove: value == "" and index != -1 //add value != "" and index == -1 vCardField.entry = idx++; if (!(vCardData.hasOwnProperty(vCardField.item) && vCardField.entry < vCardData[vCardField.item].length)) vCardField.entry = -1; //need to add a new one dav.tools.updateValueOfVCard(syncData, "Phones", vCardData, vCardField, value); } } break; default: { let value = card.getProperty(property, ""); dav.tools.updateValueOfVCard(syncData, property, vCardData, vCardField, value); } break; } } // check UID status let uidProp = card.getProperty("X-DAV-UID"); let uidItem = ""; try { uidItem = vCardData["uid"][0].value; } catch (e) {} if (!uidItem && !uidProp) { TbSync.eventlog.add("info", syncData.eventLogInfo, "Generated missing UID for card <"+listName+">"); let uid = TbSync.generateUUID(); card.setProperty("X-DAV-UID", uid); vCardData["uid"] = [{"value": uid}]; syncData.target.modifyItem(card); } else if (!uidItem && uidProp) { vCardData["uid"] = [{"value": uidProp}]; TbSync.eventlog.add("info", syncData.eventLogInfo, "Updating item uid from uid property for card <"+listName+">", JSON.stringify({uidProp, uidItem})); } else if (uidItem && !uidProp) { card.setProperty("X-DAV-UID", uidItem); TbSync.eventlog.add("info", syncData.eventLogInfo, "Updating uid property from item uid of card <"+listName+">", JSON.stringify({uidProp, uidItem})); syncData.target.modifyItem(card); } else if (uidItem != uidProp) { card.setProperty("X-DAV-UID", uidItem); TbSync.eventlog.add("info", syncData.eventLogInfo, "Updating uid property from item uid of card <"+listName+">", JSON.stringify({uidProp, uidItem})); syncData.target.modifyItem(card); } //add required fields if (!vCardData.hasOwnProperty("version")) vCardData["version"] = [{"value": "3.0"}]; if (!vCardData.hasOwnProperty("fn")) vCardData["fn"] = [{"value": " "}]; if (!vCardData.hasOwnProperty("n")) vCardData["n"] = [{"value": [" ","","","",""]}]; //build vCards let newCard = dav.vCard.generate(vCardData).trim(); let oldCard = dav.vCard.generate(cCardData).trim(); let modified = false; if (oldCard != newCard) { TbSync.dump("Card has been modified!",""); TbSync.dump("currentCard", oldCard); TbSync.dump("newCard", newCard); modified = true; } return {data: newCard, etag: card.getProperty("X-DAV-ETAG"), modified: modified}; }, } DAV-4-TbSync-1.8/content/includes/vcard/000077500000000000000000000000001362346375200177275ustar00rootroot00000000000000DAV-4-TbSync-1.8/content/includes/vcard/LICENSE000066400000000000000000000020601362346375200207320ustar00rootroot00000000000000MIT License Copyright (c) 2018 Aleksandr Kitov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. DAV-4-TbSync-1.8/content/includes/vcard/SOURCE000066400000000000000000000000651362346375200206530ustar00rootroot00000000000000https://github.com/Heymdall/vcard/releases/tag/v0.2.7DAV-4-TbSync-1.8/content/includes/vcard/vcard.js000066400000000000000000000245261362346375200213750ustar00rootroot00000000000000(function (namespace) { var PREFIX = 'BEGIN:VCARD', POSTFIX = 'END:VCARD'; /** * Return json representation of vCard * @param {string} string raw vCard * @returns {*} */ function parse(string) { var result = {}, lines = string.split(/\r\n|\r|\n/), count = lines.length, pieces, key, value, meta, namespace; for (var i = 0; i < count; i++) { if (lines[i] == '') { continue; } if (lines[i].toUpperCase() == PREFIX || lines[i].toUpperCase() == POSTFIX) { continue; } var data = lines[i]; /** * Check that next line continues current * @param {number} i * @returns {boolean} */ var isValueContinued = function (i) { return i + 1 < count && (lines[i + 1][0] == ' ' || lines[i + 1][0] == '\t'); }; // handle multiline properties (i.e. photo). // next line should start with space or tab character if (isValueContinued(i)) { while (isValueContinued(i)) { data += lines[i + 1].trim(); i++; } } pieces = data.split(':'); key = pieces.shift(); value = pieces.join(':'); namespace = false; meta = {}; // meta fields in property if (key.match(/;/)) { key = key .replace(/\\;/g, 'ΩΩΩ') .replace(/\\,/, ','); var metaArr = key.split(';').map(function (item) { return item.replace(/ΩΩΩ/g, ';'); }); key = metaArr.shift(); metaArr.forEach(function (item) { var arr = item.split('='); arr[0] = arr[0].toLowerCase(); if (arr[0].length === 0) { return; } if (arr.length>1) { //removing boundary quotes and splitting up values, if send as list - upperCase for hitory reasons let metavalue = arr[1].replace (/(^")|("$)/g, '').toUpperCase().split(","); if (meta[arr[0]]) { meta[arr[0]].push(...metavalue); } else { meta[arr[0]] = metavalue; } } }); } // values with \n value = value .replace(/\\r/g, '') .replace(/\\n/g, '\n'); value = tryToSplit(value); // Grouped properties if (key.match(/\./)) { var arr = key.split('.'); key = arr[1]; namespace = arr[0]; } var newValue = { value: value }; if (Object.keys(meta).length) { newValue.meta = meta; } if (namespace) { newValue.namespace = namespace; } if (key.indexOf('X-') !== 0) { key = key.toLowerCase(); } if (typeof result[key] === 'undefined') { result[key] = [newValue]; } else { result[key].push(newValue); } } return result; } var HAS_SEMICOLON_SEPARATOR = /[^\\];|^;/, HAS_COMMA_SEPARATOR = /[^\\],|^,/; /** * Split value by "," or ";" and remove escape sequences for this separators * @param {string} value * @returns {string|string[] */ function tryToSplit(value) { if (value.match(HAS_SEMICOLON_SEPARATOR)) { value = value.replace(/\\,/g, ','); return splitValue(value, ';'); } else if (value.match(HAS_COMMA_SEPARATOR)) { value = value.replace(/\\;/g, ';'); return splitValue(value, ','); } else { return value .replace(/\\,/g, ',') .replace(/\\;/g, ';'); } } /** * Split vcard field value by separator * @param {string|string[]} value * @param {string} separator * @returns {string|string[]} */ function splitValue(value, separator) { var separatorRegexp = new RegExp(separator); var escapedSeparatorRegexp = new RegExp('\\\\' + separator, 'g'); // easiest way, replace it with really rare character sequence value = value.replace(escapedSeparatorRegexp, 'ΩΩΩ'); if (value.match(separatorRegexp)) { value = value.split(separator); value = value.map(function (item) { return item.replace(/ΩΩΩ/g, separator); }); } else { value = value.replace(/ΩΩΩ/g, separator); } return value; } var guid = (function() { function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } return function() { return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); }; })(); var COMMA_SEPARATED_FIELDS = ['nickname', 'related', 'categories', 'pid']; /** * Generate vCard representation af object * @param {*} data * @param {obj} * member "simpleType" returns all types joined by , instead of multiple TYPE= entries * member "addRequired" determine if generator should add required properties (version and uid) * @returns {string} */ function generate(data, options = {simpleType: true}) { var lines = [PREFIX], line = ''; if (options.addRequired && !data.version) { data.version = [{value: '3.0'}]; } if (options.addRequired && !data.uid) { data.uid = [{value: guid()}]; } var escapeCharacters = function (v) { if (typeof v === 'undefined') { return ''; } return v .replace(/\r\n|\r|\n/g, '\\n') .replace(/;/g, '\\;') .replace(/,/g, '\\,') }; var escapeTypeCharacters = function(v) { if (typeof v === 'undefined') { return ''; } return v .replace(/\r\n|\r|\n/g, '\\n') .replace(/;/g, '\\;') }; Object.keys(data).forEach(function (key) { if (!data[key] || typeof data[key].forEach !== 'function') { return; } data[key].forEach(function (value) { // ignore empty values if (typeof value.value === 'undefined' || value.value == '') { return; } // ignore empty array values if (value.value instanceof Array) { var empty = true; for (var i = 0; i < value.value.length; i++) { if (typeof value.value[i] !== 'undefined' && value.value[i] != '') { empty = false; break; } } if (empty) { return; } } line = ''; // add namespace if exists if (value.namespace) { line += value.namespace + '.'; } line += key.indexOf('X-') === 0 ? key : key.toUpperCase(); // add meta properties if (typeof value.meta === 'object') { Object.keys(value.meta).forEach(function (metaKey) { // values of meta tags must be an array if (typeof value.meta[metaKey].forEach !== 'function') { return; } //join meta types so we get TYPE=a,b,c instead of TYPE=a;TYPE=b;TYPE=c let metaArr = (options.simpleType && metaKey.toUpperCase() === 'TYPE') ? [value.meta[metaKey].join(",")] : value.meta[metaKey]; metaArr.forEach(function (metaValue) { if (metaKey.length > 0) { if (metaKey.toUpperCase() === 'TYPE') { // Do not escape the comma when it is the type property. This breaks a lot. line += ';' + escapeCharacters(metaKey.toUpperCase()) + '=' + escapeTypeCharacters(metaValue); } else { line += ';' + escapeCharacters(metaKey.toUpperCase()) + '=' + escapeCharacters(metaValue); } } }); }); } line += ':'; if (typeof value.value === 'string') { line += escapeCharacters(value.value); } else { // list-values var separator = COMMA_SEPARATED_FIELDS.indexOf(key) !== -1 ? ',' : ';'; line += value.value.map(function (item) { return escapeCharacters(item); }).join(separator); } // line-length limit. Content lines // SHOULD be folded to a maximum width of 75 octets, excluding the line break. if (line.length > 75) { var firstChunk = line.substr(0, 75), least = line.substr(75); var splitted = least.match(/.{1,74}/g); lines.push(firstChunk); splitted.forEach(function (chunk) { lines.push(' ' + chunk); }); } else { lines.push(line); } }); }); lines.push(POSTFIX); return lines.join('\r\n'); } namespace.vCard = { parse: parse, generate: generate }; })(this); DAV-4-TbSync-1.8/content/manager/000077500000000000000000000000001362346375200164345ustar00rootroot00000000000000DAV-4-TbSync-1.8/content/manager/createAccount.js000066400000000000000000000614351362346375200215630ustar00rootroot00000000000000/* * This file is part of DAV-4-TbSync. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm"); const dav = TbSync.providers.dav; var tbSyncDavNewAccount = { // standard data fields get elementName() { return document.getElementById('tbsync.newaccount.name'); }, get elementUser() { return document.getElementById('tbsync.newaccount.user'); }, get elementPass() { return document.getElementById('tbsync.newaccount.password'); }, get elementServer() { return document.getElementById('tbsync.newaccount.server'); }, get elementCalDavServer() { return document.getElementById('tbsync.newaccount.caldavserver'); }, get elementCardDavServer() { return document.getElementById('tbsync.newaccount.carddavserver'); }, get serviceproviderlist() { return document.getElementById('tbsync.newaccount.serviceproviderlist'); }, get accountname() { return this.elementName.value.trim(); }, get username() { return this.elementUser.value.trim(); }, get password() { return this.elementPass.value.trim(); }, get server() { return this.elementServer.value.trim(); }, get calDavServer() { return this.elementCalDavServer.value.trim(); }, get cardDavServer() { return this.elementCardDavServer.value.trim(); }, get serviceprovider() { return this.serviceproviderlist.value; }, get userdomain() { let parts = this.username.split("@"); if (parts.length == 2) { let subparts = parts[1].split("."); if (subparts.length > 1 && subparts[subparts.length-1].length > 1) return parts[1]; } return null; }, set accountname(v) { this.elementName.value = v; }, set username(v) { this.elementUser.value = v; }, set password(v) { this.elementPass.value = v; }, set server(v) { this.elementServer.value = v; }, set calDavServer(v) { this.elementCalDavServer.value = v; }, set cardDavServer(v) { this.elementCardDavServer.value = v; }, // final data fields on final page get elementFinalName() { return document.getElementById('tbsync.finalaccount.name'); }, get elementFinalUser() { return document.getElementById('tbsync.finalaccount.user'); }, get elementFinalCalDavServer() { return document.getElementById('tbsync.finalaccount.caldavserver'); }, get elementFinalCardDavServer() { return document.getElementById('tbsync.finalaccount.carddavserver'); }, get finalAccountname() { return this.elementFinalName.value.trim(); }, get finalUsername() { return this.elementFinalUser.value.trim(); }, get finalCalDavServer() { return this.elementFinalCalDavServer.value.trim(); }, get finalCardDavServer() { return this.elementFinalCardDavServer.value.trim(); }, set finalAccountname(v) { this.elementFinalName.value = v;}, set finalUsername(v) { this.elementFinalUser.value = v; this.elementFinalUser.setAttribute("tooltiptext", v); }, set finalCalDavServer(v) { this.elementFinalCalDavServer.value = v; this.elementFinalCalDavServer.setAttribute("tooltiptext", v); document.getElementById("tbsyncfinalaccount.caldavserver.row").hidden = (v.trim() == ""); }, set finalCardDavServer(v) { this.elementFinalCardDavServer.value = v; this.elementFinalCardDavServer.setAttribute("tooltiptext", v); document.getElementById("tbsyncfinalaccount.carddavserver.row").hidden = (v.trim() == ""); }, get validated() { return this._validated || false; }, set validated(v) { this._validated = v; if (v) { this.finalAccountname = this.accountname; } else { this.finalAccountname = ""; this.finalUsername = ""; this.finalCalDavServer = ""; this.finalCardDavServer = ""; } }, showSpinner: function(spinnerText) { document.getElementById("tbsync.spinner").hidden = false; document.getElementById("tbsync.spinner.label").value = TbSync.getString("add.spinner." + spinnerText, "dav"); }, hideSpinner: function() { document.getElementById("tbsync.spinner").hidden = true; }, onLoad: function () { this.providerData = new TbSync.ProviderData("dav"); //init list this.serviceproviderlist.appendChild(this.addProviderEntry("sabredav32.png", "discovery")); this.serviceproviderlist.appendChild(this.addProviderEntry("sabredav32.png", "custom")); for (let p in dav.sync.serviceproviders) { this.serviceproviderlist.appendChild(this.addProviderEntry(dav.sync.serviceproviders[p].icon +"32.png", p)); } document.addEventListener("wizardfinish", tbSyncDavNewAccount.onFinish.bind(this)); document.addEventListener("wizardnext", tbSyncDavNewAccount.onAdvance.bind(this)); document.addEventListener("wizardcancel", tbSyncDavNewAccount.onCancel.bind(this)); document.getElementById("firstPage").addEventListener("pageshow", tbSyncDavNewAccount.resetFirstPage.bind(this)); document.getElementById("secondPage").addEventListener("pageshow", tbSyncDavNewAccount.resetSecondPage.bind(this)); document.getElementById("thirdPage").addEventListener("pageshow", tbSyncDavNewAccount.resetThirdPage.bind(this)); this.serviceproviderlist.selectedIndex = 0; tbSyncDavNewAccount.resetFirstPage(); }, onUnload: function () { }, onClose: function () { //disallow closing of wizard while isLocked return !this.isLocked; }, onCancel: function (event) { //disallow closing of wizard while isLocked if (this.isLocked) { event.preventDefault(); } }, onFinish () { let newAccountEntry = this.providerData.getDefaultAccountEntries(); newAccountEntry.createdWithProviderVersion = this.providerData.getVersion(); newAccountEntry.serviceprovider = this.serviceprovider == "discovery" ? "custom" : this.serviceprovider; newAccountEntry.serviceproviderRevision = dav.sync.serviceproviders.hasOwnProperty(this.serviceprovider) ? dav.sync.serviceproviders[this.serviceprovider].revision : 0 newAccountEntry.calDavHost = this.finalCalDavServer; newAccountEntry.cardDavHost = this.finalCardDavServer; // Add the new account. let newAccountData = this.providerData.addAccount(this.finalAccountname, newAccountEntry); dav.network.getAuthData(newAccountData).updateLoginData(this.finalUsername, this.password); }, // HELPER FUNCTIONS addProviderEntry: function (icon, serviceprovider) { let name = TbSync.getString("add.serverprofile."+serviceprovider, "dav"); let description = TbSync.getString("add.serverprofile."+serviceprovider+".description", "dav"); //left column let image = document.createXULElement("image"); image.setAttribute("src", "chrome://dav4tbsync/skin/" + icon); image.setAttribute("style", "margin:1ex;"); let leftColumn = document.createXULElement("vbox"); leftColumn.appendChild(image); //right column let label = document.createXULElement("label"); label.setAttribute("class", "header"); label.setAttribute("value", name); let desc = document.createXULElement("description"); desc.setAttribute("style", "width: 300px"); desc.textContent = description; let rightColumn = document.createXULElement("vbox"); rightColumn.appendChild(label); rightColumn.appendChild(desc); //columns let columns = document.createXULElement("hbox"); columns.appendChild(leftColumn); columns.appendChild(rightColumn); //richlistitem let richlistitem = document.createXULElement("richlistitem"); richlistitem.setAttribute("style", "padding:4px"); richlistitem.setAttribute("value", serviceprovider); richlistitem.appendChild(columns); return richlistitem; }, checkUrlForPrincipal: async function (job) { // according to RFC6764, we must also try the username with cut-off domain part // Note: This is never called for OAUTH serves (see onAdvance) let users = []; users.push(this.username); if (this.userdomain) users.push(this.username.split("@")[0]); for (let user of users) { let connectionData = new dav.network.ConnectionData(); connectionData.password = this.password; connectionData.username = user; connectionData.timeout = 5000; //only needed for proper error reporting - that dav needs this is beyond API - connectionData is not used by TbSync //connectionData is a structure which contains all the information needed to establish and evaluate a network connection connectionData.eventLogInfo = new TbSync.EventLogInfo("dav", this.accountname); job.valid = false; job.error = ""; try { let response = await dav.network.sendRequest("", job.server , "PROPFIND", connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {containerRealm: "setup", containerReset: true, passwordRetries: 0}); // allow 404 because iCloud sends it on valid answer (yeah!) let principal = (response && response.multi) ? dav.tools.getNodeTextContentFromMultiResponse(response, [["d","prop"], ["d","current-user-principal"], ["d","href"]], null, ["200","404"]) : null; job.valid = (principal !== null); if (!job.valid) { job.error = job.type + "servernotfound"; TbSync.eventlog.add("warning", connectionData.eventLogInfo, job.error, response ? response.commLog : ""); } else { job.validUser = user; job.validUrl = (response ? response.permanentlyRedirectedUrl : null) || job.server; return; } } catch (e) { job.valid = false; job.error = (e.statusData ? e.statusData.message : e.message); if (e.name == "dav4tbsync") { TbSync.eventlog.add("warning", connectionData.eventLogInfo, e.statusData.message, e.statusData.details); } else { Components.utils.reportError(e); } } // only retry with other user, if 401 if (!job.error.startsWith("401")) { break; } } return; }, advance: function () { document.getElementById("tbsync.newaccount.wizard").advance(null); }, // RESET AND INIT FUNCTIONS clearValues: function () { //clear fields this.username = ""; this.password = ""; this.server = ""; this.calDavServer = ""; this.cardDavServer = ""; if (this.serviceprovider == "discovery" || this.serviceprovider == "custom") { this.accountname = ""; } else { this.accountname = TbSync.getString("add.serverprofile." + this.serviceprovider, "dav"); } }, resetFirstPage: function () { // RESET / INIT first page document.getElementById("tbsync.newaccount.wizard").canRewind = false; document.getElementById("tbsync.newaccount.wizard").canAdvance = true; this.isLocked = false; this.validated = false; }, resetSecondPage: function () { // RESET / INIT second page this.isLocked = false; this.validated = false; document.getElementById("tbsync.newaccount.wizard").canRewind = true; document.getElementById("tbsync.newaccount.wizard").canAdvance = false; this.hideSpinner(); document.getElementById("tbsync.error").hidden = true; this.checkUI(); }, resetThirdPage: function () { // RESET / INIT third page document.getElementById("tbsync.newaccount.wizard").canRewind = true; document.getElementById("tbsync.newaccount.wizard").canAdvance = true; this.isLocked = false; }, // UI FUNCTIONS lockUI: function(spinnerText) { this.showSpinner(spinnerText); document.getElementById("tbsync.error").hidden = true; document.getElementById("tbsync.newaccount.wizard").canAdvance = false; document.getElementById("tbsync.newaccount.wizard").canRewind = false; this.isLocked = true; }, unlockUI: function() { this.hideSpinner(); document.getElementById("tbsync.newaccount.wizard").canRewind = true; this.isLocked = false; this.checkUI(); }, checkUI: function (hideError) { if (hideError) { document.getElementById("tbsync.error").hidden = true; } // determine, if we can advance or not if (this.serviceprovider == "discovery") { document.getElementById("tbsync.newaccount.wizard").canAdvance = !( (this.accountname == "") || (this.server == "" && !this.userdomain) || (this.server == "" && this.username == "")); } else if (this.serviceprovider == "custom") { // custom does not need username or password (allow annonymous access) document.getElementById("tbsync.newaccount.wizard").canAdvance = !( (this.accountname == "") || (this.calDavServer + this.cardDavServer == "")); } else if (this.serviceprovider == "google") { // google does not need a password field and also no username document.getElementById("tbsync.newaccount.wizard").canAdvance = !( (this.accountname == "")); } else { // build in service providers do need a username and password document.getElementById("tbsync.newaccount.wizard").canAdvance = !( (this.accountname == "") || (this.password == "") || (this.username == "")); } // update placeholder attribute of server this.elementServer.setAttribute("placeholder", this.userdomain ? TbSync.getString("add.serverprofile.discovery.server-optional", "dav") : ""); //show/hide additional descriptions (if avail) let dFound = 0; for (let i=1; i < 4; i++) { let dElement = document.getElementById("tbsync.newaccount.details" + i); let dLocaleString = "add.serverprofile." + this.serviceprovider + ".details" + i; let dLocaleValue = TbSync.getString(dLocaleString, "dav"); let hide = (dLocaleValue == dLocaleString); if (this.serviceprovider == "discovery") { // show them according to UI state switch (i) { case 1: hide = false; break; case 2: hide = !this.userdomain; break; } } if (hide) { dElement.textContent = ""; dElement.hidden = true; } else { dFound++; dElement.textContent = dLocaleValue dElement.hidden =false; } } //hide Notes header, if no descriptions avail let dLabel = document.getElementById("tbsync.newaccount.details.header"); dLabel.hidden = (dFound == 0); //which server fields to show? document.getElementById("tbsync.newaccount.finaluser.row").hidden = (this.serviceprovider == "google"); document.getElementById("tbsync.newaccount.user.row").hidden = (this.serviceprovider == "google"); document.getElementById("tbsync.newaccount.password.row").hidden = (this.serviceprovider == "google"); if (this.serviceprovider == "discovery") { document.getElementById("tbsync.newaccount.caldavserver.row").hidden = true; document.getElementById("tbsync.newaccount.carddavserver.row").hidden = true; document.getElementById("tbsync.newaccount.server.row").hidden = false; //this.elementCalDavServer.disabled = false; //this.elementCardDavServer.disabled = false; } else if (this.serviceprovider == "custom") { // custom document.getElementById("tbsync.newaccount.caldavserver.row").hidden = false; document.getElementById("tbsync.newaccount.carddavserver.row").hidden = false; document.getElementById("tbsync.newaccount.server.row").hidden = true; //this.elementCalDavServer.disabled = false; //this.elementCardDavServer.disabled = false; } else { // build in service provider document.getElementById("tbsync.newaccount.caldavserver.row").hidden = true; document.getElementById("tbsync.newaccount.carddavserver.row").hidden = true; document.getElementById("tbsync.newaccount.server.row").hidden = true; //this.elementCalDavServer.disabled = true; //this.elementCardDavServer.disabled = true; this.calDavServer = dav.sync.serviceproviders[this.serviceprovider].caldav; this.cardDavServer = dav.sync.serviceproviders[this.serviceprovider].carddav; } }, // SETUP LOGIC FUNCTION onAdvance: function (event) { // do not prevent advancing if we go from page 1 to page 2, or if validation succeeded if (document.getElementById("tbsync.newaccount.wizard").currentPage.id == "firstPage" || this.validated) { return; } // if we reach this, we are on page 2 but may not advance but // go through the setup steps if (this.serviceprovider == "discovery") { while (this.server.endsWith("/")) { this.server = this.server.slice(0,-1); } // the user may either specify a server or he could have entered an email with domain let parts = (this.server || this.userdomain).split("://"); let scheme = (parts.length > 1) ? parts[0].toLowerCase() : ""; let host = parts[parts.length-1]; this.calDavServer = scheme + "caldav6764://" + host; this.cardDavServer = scheme + "carddav6764://" + host; this.validateDavServers(); } else if (this.serviceprovider == "google") { // do not verify, just prompt for permissions this.promptForOAuth(); } else { // custom or service provider this.validateDavServers(); } event.preventDefault(); }, promptForOAuth: async function() { this.lockUI("validating"); let oauthData = dav.network.getOAuthObj(this.calDavServer, { username: this.username, accountname: this.accountname }); if (oauthData) { let rv = {}; if (await oauthData.asyncConnect(rv)) { this.password = rv.tokens; this.finalCalDavServer = this.calDavServer; this.finalCardDavServer = this.cardDavServer; this.finalUsername = this.username; this.validated = true; this.unlockUI(); this.advance(); return; } else { document.getElementById("tbsync.error.message").textContent = TbSync.getString("status." + rv.error, "dav"); document.getElementById("tbsync.error").hidden = false; this.unlockUI(); return; } } document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.OAuthNetworkError", "dav"); document.getElementById("tbsync.error").hidden = false; this.unlockUI(); }, validateDavServers: async function() { this.lockUI("validating"); // Do not manipulate input here. //while (this.calDavServer.endsWith("/")) { this.calDavServer = this.calDavServer.slice(0,-1); } //while (this.cardDavServer.endsWith("/")) { this.cardDavServer = this.cardDavServer.slice(0,-1); } // Default to https, if http is not explicitly specified if (this.calDavServer && !dav.network.startsWithScheme(this.calDavServer)) { this.calDavServer = "https://" + this.calDavServer; } if (this.cardDavServer && !dav.network.startsWithScheme(this.cardDavServer)) { this.cardDavServer = "https://" + this.cardDavServer; } let davJobs = [ {type: "caldav", server: this.calDavServer}, {type: "carddav", server: this.cardDavServer}, ]; let failedDavJobs = []; let validUserFound = ""; for (let job = 0; job < davJobs.length; job++) { if (!davJobs[job].server) { continue; } await this.checkUrlForPrincipal(davJobs[job]); if (!davJobs[job].valid) { failedDavJobs.push(job); } else if (!validUserFound) { // set the found user validUserFound = davJobs[job].validUser; } else if (validUserFound != davJobs[job].validUser) { // users do not match failedDavJobs.push(job); } } if (failedDavJobs.length == 0) { // boom, setup completed this.finalCalDavServer = davJobs[0].validUrl || ""; this.finalCardDavServer = davJobs[1].validUrl || ""; this.finalUsername = validUserFound; this.validated = true; this.unlockUI(); this.advance(); return; } else { //only display one error let failedJob = failedDavJobs[0]; console.log("ERROR ("+davJobs[failedJob].type+"): " + davJobs[failedJob].error.toString()); switch (davJobs[failedJob].error.toString().split("::")[0]) { case "401": case "403": case "503": case "security": document.getElementById("tbsync.error.message").textContent = TbSync.getString("status."+davJobs[failedJob].error, "dav"); break; default: if (this.serviceprovider == "discovery" && this.userdomain && !this.server) { // the discovery mode has a special error msg, in case a userdomain was used as server, but failed and we need the user to provide the server document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.rfc6764-lookup-failed::" +this.userdomain, "dav"); } else if (this.serviceprovider != "discovery" && this.serviceprovider != "custom") { // error msg, that the serviceprovider setup seems wrong document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.service-provider-setup-failed", "dav"); } else if (dav.network.isRFC6764Request(davJobs[failedJob].server)) { // error msg, that discovery mode failed document.getElementById("tbsync.error.message").textContent = TbSync.getString("status.service-discovery-failed::" +davJobs[failedJob].server.split("://")[1], "dav"); } else { document.getElementById("tbsync.error.message").textContent = TbSync.getString("status." + davJobs[failedJob].type + "servernotfound", "dav"); } } document.getElementById("tbsync.error").hidden = false; this.unlockUI(); } }, }; DAV-4-TbSync-1.8/content/manager/createAccount.xul000066400000000000000000000137121362346375200217520ustar00rootroot00000000000000 %tbsyncDTD; %davDTD; ]>