summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2021-03-23 05:55:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2021-03-23 05:55:22 +0000
commitb7b7cab0f825ed6a600898f42a149551919acd0a (patch)
tree1da062ca81e963ae7608e330c1820726143ae853
parentInitial commit. (diff)
downloaddav4tbsync-b7b7cab0f825ed6a600898f42a149551919acd0a.tar.xz
dav4tbsync-b7b7cab0f825ed6a600898f42a149551919acd0a.zip
Adding upstream version 1.23.upstream/1.23upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.github/issue_template.md21
-rw-r--r--CONTRIBUTORS.md18
-rw-r--r--LICENSE373
-rw-r--r--Makebeta34
-rw-r--r--Makefile.bat11
-rw-r--r--README.md35
-rw-r--r--_locales/Readme.txt8
-rw-r--r--_locales/bg/messages.json356
-rw-r--r--_locales/cs/messages.json356
-rw-r--r--_locales/de/messages.json356
-rw-r--r--_locales/en-US/messages.json356
-rw-r--r--_locales/fr/messages.json356
-rw-r--r--_locales/hu/messages.json356
-rw-r--r--_locales/it/messages.json356
-rw-r--r--_locales/ja/messages.json356
-rw-r--r--_locales/pl/messages.json356
-rw-r--r--_locales/pt_BR/messages.json356
-rw-r--r--_locales/ru/messages.json356
-rw-r--r--background.js13
-rw-r--r--beta-release-channel-update.json13
-rw-r--r--content/api/BootstrapLoader/README.md1
-rw-r--r--content/api/BootstrapLoader/implementation.js153
-rw-r--r--content/api/BootstrapLoader/schema.json39
-rw-r--r--content/bootstrap.js77
-rw-r--r--content/includes/GoogleDavCalendar.jsm2436
-rw-r--r--content/includes/GoogleDavSession.jsm132
-rw-r--r--content/includes/abUI.js476
-rw-r--r--content/includes/network.js649
-rw-r--r--content/includes/sync.js910
-rw-r--r--content/includes/tools.js1301
-rw-r--r--content/includes/vcard/LICENSE21
-rw-r--r--content/includes/vcard/SOURCE1
-rw-r--r--content/includes/vcard/vcard.js305
-rw-r--r--content/locales.js3
-rw-r--r--content/manager/createAccount.js559
-rw-r--r--content/manager/createAccount.xhtml123
-rw-r--r--content/manager/editAccountOverlay.js46
-rw-r--r--content/manager/editAccountOverlay.xhtml90
-rw-r--r--content/overlays/abCSS.xhtml8
-rw-r--r--content/overlays/abCardWindow.js143
-rw-r--r--content/overlays/abCardWindow.xhtml91
-rw-r--r--content/overlays/abNewCardWindow.js30
-rw-r--r--content/overlays/abNewCardWindow.xhtml14
-rw-r--r--content/overlays/addressbookdetailsoverlay.js136
-rw-r--r--content/overlays/addressbookdetailsoverlay.xhtml34
-rw-r--r--content/overlays/addressbookoverlay.js42
-rw-r--r--content/overlays/addressbookoverlay.xhtml10
-rw-r--r--content/provider.js785
-rw-r--r--content/skin/ab.css82
-rw-r--r--content/skin/arrow.down10.pngbin0 -> 17764 bytes
-rw-r--r--content/skin/arrow.up10.pngbin0 -> 17762 bytes
-rw-r--r--content/skin/dragdrop.pngbin0 -> 18111 bytes
-rw-r--r--content/skin/fruux16.pngbin0 -> 788 bytes
-rw-r--r--content/skin/fruux32.pngbin0 -> 22319 bytes
-rw-r--r--content/skin/fruux48.pngbin0 -> 27455 bytes
-rw-r--r--content/skin/gmx16.pngbin0 -> 416 bytes
-rw-r--r--content/skin/gmx32.pngbin0 -> 896 bytes
-rw-r--r--content/skin/gmx48.pngbin0 -> 1386 bytes
-rw-r--r--content/skin/google16.pngbin0 -> 605 bytes
-rw-r--r--content/skin/google32.pngbin0 -> 1155 bytes
-rw-r--r--content/skin/google48.pngbin0 -> 2304 bytes
-rw-r--r--content/skin/icloud16.pngbin0 -> 427 bytes
-rw-r--r--content/skin/icloud32.pngbin0 -> 769 bytes
-rw-r--r--content/skin/icloud48.pngbin0 -> 1196 bytes
-rw-r--r--content/skin/ics16.pngbin0 -> 789 bytes
-rw-r--r--content/skin/mbo16.pngbin0 -> 535 bytes
-rw-r--r--content/skin/mbo32.pngbin0 -> 14636 bytes
-rw-r--r--content/skin/mbo48.pngbin0 -> 1172 bytes
-rw-r--r--content/skin/posteo16.pngbin0 -> 461 bytes
-rw-r--r--content/skin/posteo32.pngbin0 -> 742 bytes
-rw-r--r--content/skin/posteo48.pngbin0 -> 922 bytes
-rw-r--r--content/skin/sabredav16.pngbin0 -> 18198 bytes
-rw-r--r--content/skin/sabredav32.pngbin0 -> 19228 bytes
-rw-r--r--content/skin/sabredav48.pngbin0 -> 2825 bytes
-rw-r--r--content/skin/type.car10.pngbin0 -> 18319 bytes
-rw-r--r--content/skin/type.car16.pngbin0 -> 17917 bytes
-rw-r--r--content/skin/type.cell10.pngbin0 -> 18495 bytes
-rw-r--r--content/skin/type.cell16.pngbin0 -> 18517 bytes
-rw-r--r--content/skin/type.fax10.pngbin0 -> 18312 bytes
-rw-r--r--content/skin/type.fax16.pngbin0 -> 17919 bytes
-rw-r--r--content/skin/type.home10.pngbin0 -> 18247 bytes
-rw-r--r--content/skin/type.home16.pngbin0 -> 18283 bytes
-rw-r--r--content/skin/type.nopref.pngbin0 -> 15977 bytes
-rw-r--r--content/skin/type.other10.pngbin0 -> 18325 bytes
-rw-r--r--content/skin/type.other16.pngbin0 -> 18368 bytes
-rw-r--r--content/skin/type.pager10.pngbin0 -> 18450 bytes
-rw-r--r--content/skin/type.pager16.pngbin0 -> 18471 bytes
-rw-r--r--content/skin/type.pref.pngbin0 -> 18261 bytes
-rw-r--r--content/skin/type.video10.pngbin0 -> 18298 bytes
-rw-r--r--content/skin/type.video16.pngbin0 -> 18339 bytes
-rw-r--r--content/skin/type.voice10.pngbin0 -> 18270 bytes
-rw-r--r--content/skin/type.voice16.pngbin0 -> 17864 bytes
-rw-r--r--content/skin/type.work10.pngbin0 -> 18257 bytes
-rw-r--r--content/skin/type.work16.pngbin0 -> 18287 bytes
-rw-r--r--content/skin/web16.pngbin0 -> 564 bytes
-rw-r--r--content/skin/web32.pngbin0 -> 1419 bytes
-rw-r--r--content/skin/web48.pngbin0 -> 2301 bytes
-rw-r--r--content/skin/yahoo16.pngbin0 -> 561 bytes
-rw-r--r--content/skin/yahoo32.pngbin0 -> 1103 bytes
-rw-r--r--content/skin/yahoo48.pngbin0 -> 1640 bytes
-rw-r--r--crowdin.yml5
-rw-r--r--manifest.json32
-rw-r--r--screenshots/1.pngbin0 -> 32030 bytes
-rw-r--r--screenshots/2.pngbin0 -> 32775 bytes
-rw-r--r--screenshots/3.pngbin0 -> 30577 bytes
-rw-r--r--screenshots/4.pngbin0 -> 17432 bytes
-rw-r--r--screenshots/5.pngbin0 -> 19577 bytes
-rw-r--r--screenshots/6.pngbin0 -> 65965 bytes
-rw-r--r--screenshots/AddAccount.pngbin0 -> 88496 bytes
-rw-r--r--screenshots/custom-service.PNGbin0 -> 41908 bytes
-rw-r--r--screenshots/discoveryservice.PNGbin0 -> 41954 bytes
-rw-r--r--screenshots/icloud.pngbin0 -> 15837 bytes
-rw-r--r--screenshots/icloudlist.pngbin0 -> 62197 bytes
-rw-r--r--screenshots/install.pngbin0 -> 27787 bytes
-rw-r--r--screenshots/options.pngbin0 -> 74839 bytes
-rw-r--r--screenshots/posteo.PNGbin0 -> 23884 bytes
-rw-r--r--screenshots/sercviceselection.pngbin0 -> 34688 bytes
-rw-r--r--unused/_mbo16.pngbin0 -> 349 bytes
-rw-r--r--unused/_mbo32.pngbin0 -> 675 bytes
-rw-r--r--unused/_mbo48.pngbin0 -> 1040 bytes
-rw-r--r--unused/google13
-rw-r--r--unused/iconfinder_17_939744_16.pngbin0 -> 327 bytes
-rw-r--r--unused/iconfinder_17_939744_32.pngbin0 -> 619 bytes
-rw-r--r--unused/iconfinder_17_939744_64.pngbin0 -> 1217 bytes
-rw-r--r--unused/iconfinder_Apple_1298725_16.pngbin0 -> 436 bytes
-rw-r--r--unused/iconfinder_Apple_1298725_32.pngbin0 -> 826 bytes
-rw-r--r--unused/iconfinder_Apple_1298725_64.pngbin0 -> 1712 bytes
-rw-r--r--unused/mbo48_2.pngbin0 -> 940 bytes
128 files changed, 13189 insertions, 0 deletions
diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 0000000..c3a36f7
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,21 @@
+## Your environment
+
+TbSync version:
+DAV-4-TbSync version:
+Thunderbird version:
+
+[ ] Yes, I have installed the latest available beta version from
+https://tbsync.jobisoft.de
+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).
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
new file mode 100644
index 0000000..58cdd82
--- /dev/null
+++ b/CONTRIBUTORS.md
@@ -0,0 +1,18 @@
+## 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)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla 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.
diff --git a/Makebeta b/Makebeta
new file mode 100644
index 0000000..24ef73f
--- /dev/null
+++ b/Makebeta
@@ -0,0 +1,34 @@
+#!/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 manifest.json background.js LICENSE CONTRIBUTORS.md
+tar cfvz $3.tar.gz content _locales manifest.json background.js LICENSE CONTRIBUTORS.md
+cp $3 $2/$3
+cp $3.tar.gz $2/$3.tar.gz
diff --git a/Makefile.bat b/Makefile.bat
new file mode 100644
index 0000000..bde1c1d
--- /dev/null
+++ b/Makefile.bat
@@ -0,0 +1,11 @@
+@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 manifest.json background.js LICENSE CONTRIBUTORS.md
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c5972ed
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+# 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/)
+
diff --git a/_locales/Readme.txt b/_locales/Readme.txt
new file mode 100644
index 0000000..10208fa
--- /dev/null
+++ b/_locales/Readme.txt
@@ -0,0 +1,8 @@
+Want 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.
diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json
new file mode 100644
index 0000000..d8e3768
--- /dev/null
+++ b/_locales/bg/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Свойства на контакта (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Електронни адреси:"
+ },
+ "abCard.MiddleName": {
+ "message": "Бащино име:"
+ },
+ "abCard.Phone": {
+ "message": "Телефонни номера"
+ },
+ "abCard.PrefixName": {
+ "message": "Представка:"
+ },
+ "abCard.SuffixName": {
+ "message": "Наставка:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Телефон в колата"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Мобилен телефон"
+ },
+ "abCard.emailtypes.description": {
+ "message": "Първият електронен адрес от списъка ще бъде ползван като основен."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Факс"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Лични"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Други"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Пейджър"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Видео"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Телефон"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Служебни"
+ },
+ "acl.add": {
+ "message": "вмъквате"
+ },
+ "acl.delete": {
+ "message": "изтриване"
+ },
+ "acl.modify": {
+ "message": "промяна"
+ },
+ "acl.none": {
+ "message": "никакви"
+ },
+ "acl.readonly": {
+ "message": "Достъп до сървъра само за четене (изтрива местните промени)"
+ },
+ "acl.readwrite": {
+ "message": "Права за писане на сървъра: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "CalDAV сървър:"
+ },
+ "add.carddavserver": {
+ "message": "CardDAV сървър:"
+ },
+ "add.data.description": {
+ "message": "Въведете име за новата TbSync регистрация и данните за връзка със сървъра ви:"
+ },
+ "add.data.notes": {
+ "message": "Бележка:"
+ },
+ "add.data.title": {
+ "message": "Въвеждане на регистрацията"
+ },
+ "add.finish.description": {
+ "message": "Следните настройки бяха потвърдени:"
+ },
+ "add.finish.details": {
+ "message": "Натиснете „Готово“ за да създадете нова TbSync регистрация с тези настройки."
+ },
+ "add.finish.title": {
+ "message": "Потвърдете създаването на регистрация"
+ },
+ "add.name": {
+ "message": "Име на регистрация:"
+ },
+ "add.ok": {
+ "message": "Добавяне на регистрация"
+ },
+ "add.password": {
+ "message": "Парола:"
+ },
+ "add.server": {
+ "message": "Адрес на сървър:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Ръчно настройване"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "Адресите за CalDAV и CardDAV услугите може да се настроят ръчно."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "Необходимите CalDAV и CardDAV адреси, съответно WebDAV адреса на потребителя, ще ви ги даде вашият доставчик."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "Ако оставите един от двата адреса празни, съответната услуга ще бъде изключена за вашата регистрация."
+ },
+ "add.serverprofile.description": {
+ "message": "Изберете един от наличните сървърни профили:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Автоматична настройка"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "Повечето сървъри поддържат автоматично настойване, за което е нужно да въведете само електронен адрес или потребителско име и сървър."
+ },
+ "add.serverprofile.discovery.details1": {
+ "message": "Въведете за автоматичното откриване на CalDAV и CardDAV адресите вашите данни за вход и съответния сървър (н.пр. “cloud.example.bg”)"
+ },
+ "add.serverprofile.discovery.details2": {
+ "message": "Ако потребителското ви име е адрес на електронна поща, указването на сървър не е задължително, тъй като информацията за настройките може да се извлече от доставчика (по RFC 6764)."
+ },
+ "add.serverprofile.discovery.server-optional": {
+ "message": "по желание"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "fruux е услуга, която синхронизира контакти, календари и задачи. Услугата се предлага от фирмата зад sabre/dav и седалището ѝ е в Германия."
+ },
+ "add.serverprofile.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europa)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Гугъл"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "Гугъл използва съвременен подход за удостоверяване без потребителско име и парола. След като изберете „Напред >“, ще се пръкне джам. Влезте чрез него в Гугъл и позволете на “Provider for CalDAV & CardDAV” достъп до данните ви."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "Потребителското име е вашият Apple ID. Паролата обаче не е паролата за вашия Apple ID! За да синхронизира TbSync вашите контакти и календари е необходимо да включите регистрацията с два фактора (2FA) за вашата iCloud-регистрация и да създадете отделна парола за приложението TbSync."
+ },
+ "add.serverprofile.icloud.details2": {
+ "message": "Това е допълнително ниво на защита, наложено от Apple, което не дава достъп на приложенията до вашата Apple-регистрация."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "mailbox.org е германски доставчик от германия с акцент върху сигурността за частни и бизнес клиенти. Предлага календари, контакти и място в облака."
+ },
+ "add.serverprofile.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Избор на сървърен профил"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Изпращане на RFC 6764 запитване до „##replace.1##“"
+ },
+ "add.spinner.validating": {
+ "message": "Проверяване на връзката към сървъра"
+ },
+ "add.title": {
+ "message": "Добавяне на CalDAV и CardDAV регистрация към TbSync"
+ },
+ "add.user": {
+ "message": "Потребителско име:"
+ },
+ "autocomplete.HOME": {
+ "message": "личен"
+ },
+ "autocomplete.PREF": {
+ "message": "предпочитан"
+ },
+ "autocomplete.WORK": {
+ "message": "служебен"
+ },
+ "config.custom": {
+ "message": "CalDAV и CardDAV настройки на сървъра"
+ },
+ "defaultname.calendar": {
+ "message": "Календар"
+ },
+ "defaultname.contacts": {
+ "message": "Контакти"
+ },
+ "extensionDescription": {
+ "message": "Разширява TbSync и позволява синхронизацията с CalDAV и CardDAV регистрации (контакти, задачи, календари)."
+ },
+ "extensionName": {
+ "message": "Доставчик за CalDAV и CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Настройки на регистрацията"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Опции"
+ },
+ "menu.name": {
+ "message": "CalDAV и CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Име на регистрация"
+ },
+ "pref.CalDavServer": {
+ "message": "CalDAV сървър:"
+ },
+ "pref.CardDavServer": {
+ "message": "CardDAV сървър:"
+ },
+ "pref.UserName": {
+ "message": "Потребителско име"
+ },
+ "pref.calendaroptions": {
+ "message": "Настройки за календара"
+ },
+ "pref.contactoptions": {
+ "message": "Настройки за контакта"
+ },
+ "pref.downloadonly": {
+ "message": "Пренебрегване на местните промени (еднопосочно синхронизаране)"
+ },
+ "pref.generaloptions": {
+ "message": "Общи настройки"
+ },
+ "pref.syncGroups": {
+ "message": "Групите контакти ги синхронизирай като Thunderbird списъци"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "Забележка: Заради TB грешка 1522453 TbSync трябва към всички контакти, които нямат електронен адрес и принадлежат към група, да добави някакъв електронен адрес. Този вмъкнат адрес не се синхронизира със сървъра."
+ },
+ "pref.useCalendarCache": {
+ "message": "Офлайн поддръжка"
+ },
+ "pref.useCardBook": {
+ "message": "Създай адресен указател в CardBook, вместо в стандартния адресен указател на Thunderbird"
+ },
+ "status.401": {
+ "message": "Достъпът отказан, проверете потребителското име и паролата."
+ },
+ "status.403": {
+ "message": "Връзката беше отхвърнена от сървъра (забранена)."
+ },
+ "status.404": {
+ "message": "HTTP грешка 404 (поисканият ресурс не беше намерен)."
+ },
+ "status.500": {
+ "message": "Непозната грешка от сървъра (HTTP грешка 500)."
+ },
+ "status.503": {
+ "message": "Услугата е недостъпна."
+ },
+ "status.caldavservernotfound": {
+ "message": "Не беше намерен CalDAV сървър."
+ },
+ "status.carddavservernotfound": {
+ "message": "Не беше намерен CardDAV сървър."
+ },
+ "status.gContactSync": {
+ "message": "Има несъвместимост с gContactSync при включена синхронизация на групите от контакти. Изключете едно от двете, докато грешката не бъде отстранена."
+ },
+ "status.info.restored": {
+ "message": "Поради частично недостатъчни права за правене на промени, някои действия бяха отхвърлени от сървъра и местните промени бяха изтрити."
+ },
+ "status.malformed-xml": {
+ "message": "Отговорът не е правилен XML. Синхронизацията беше прекъсната. Проверете протокола със събитията за подробности."
+ },
+ "status.missing-permission": {
+ "message": "Недостатъчни права: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Не можах да се свръжа със сървъра."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "Запитването за „##replace.1##“ не намери адресите за CalDAV и CardDAV услугите. Въведете името на сървъра, за да продължи настройването."
+ },
+ "status.service-discovery-failed": {
+ "message": "Откриването на настройките по RFC6764 за „##replace.1##“ не проработи. Въведете друг адрес на сървър или направете ръчно настройване, за да въведете сами адресите."
+ },
+ "status.softerror": {
+ "message": "Пренебрегната грешка (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Обновяване на списъка с ресурсите"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Обработка на потвърждението за местните промени"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Обработка на новите данни от сървъра"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Изпращане на местните промени"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Запитване за списък с ресурсите"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Изчакване за потвърждение, че местните промени са изпратени"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Изчакване на данни от сървъра за промени"
+ }
+} \ No newline at end of file
diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json
new file mode 100644
index 0000000..c74516e
--- /dev/null
+++ b/_locales/cs/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Contact properties (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "E-mail addresses"
+ },
+ "abCard.MiddleName": {
+ "message": "Middle:"
+ },
+ "abCard.Phone": {
+ "message": "Phone numbers"
+ },
+ "abCard.PrefixName": {
+ "message": "Prefix:"
+ },
+ "abCard.SuffixName": {
+ "message": "Suffix:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Car"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Mobile"
+ },
+ "abCard.emailtypes.description": {
+ "message": "The first e-mail address in the list will be used as the primary e-mail address."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Home"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Other"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Video"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Phone"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Work"
+ },
+ "acl.add": {
+ "message": "add"
+ },
+ "acl.delete": {
+ "message": "delete"
+ },
+ "acl.modify": {
+ "message": "modify"
+ },
+ "acl.none": {
+ "message": "none"
+ },
+ "acl.readonly": {
+ "message": "Read-only server access (revert local changes)"
+ },
+ "acl.readwrite": {
+ "message": "Server write permissions: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "CalDAV server address:"
+ },
+ "add.carddavserver": {
+ "message": "CardDAV server address:"
+ },
+ "add.data.description": {
+ "message": "Please provide a friendly name for the new TbSync account and the credentials for your server:"
+ },
+ "add.data.notes": {
+ "message": "Notes:"
+ },
+ "add.data.title": {
+ "message": "Enter account information"
+ },
+ "add.finish.description": {
+ "message": "The following settings have been verified successfully:"
+ },
+ "add.finish.details": {
+ "message": "Click “Finish” to create a new TbSync account with these settings."
+ },
+ "add.finish.title": {
+ "message": "Confirm account creation"
+ },
+ "add.name": {
+ "message": "Account name:"
+ },
+ "add.ok": {
+ "message": "Add account"
+ },
+ "add.password": {
+ "message": "Password:"
+ },
+ "add.server": {
+ "message": "Server URL:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Manual Configuration"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "The CalDAV and CardDAV service endpoints can be configured manually."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "The required CalDAV & CardDAV service endpoints or the so called principal addresses should be provided by your service provider."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "If you leave either address empty, the corresponding service will be disabled for this account."
+ },
+ "add.serverprofile.description": {
+ "message": "Please select one of the available server profiles:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Automatic Configuration"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "optional"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europe)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "This is a security layer enforced by Apple, so that 3rd party clients do not gain access to your Apple account."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage."
+ },
+ "add.serverprofile.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Select a server profile"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Sending RFC6764 request to “##replace.1##”"
+ },
+ "add.spinner.validating": {
+ "message": "Verifying connection to server"
+ },
+ "add.title": {
+ "message": "Adding a CalDAV & CardDAV account to TbSync"
+ },
+ "add.user": {
+ "message": "User name:"
+ },
+ "autocomplete.HOME": {
+ "message": "private"
+ },
+ "autocomplete.PREF": {
+ "message": "preferred"
+ },
+ "autocomplete.WORK": {
+ "message": "business"
+ },
+ "config.custom": {
+ "message": "CalDAV & CardDAV server configuration"
+ },
+ "defaultname.calendar": {
+ "message": "Calendar"
+ },
+ "defaultname.contacts": {
+ "message": "Contacts"
+ },
+ "extensionDescription": {
+ "message": "Add sync support for CalDAV & CardDAV accounts to TbSync."
+ },
+ "extensionName": {
+ "message": "Provider for CalDAV & CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Account settings"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Options"
+ },
+ "menu.name": {
+ "message": "CalDAV & CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Account name"
+ },
+ "pref.CalDavServer": {
+ "message": "CalDAV server address:"
+ },
+ "pref.CardDavServer": {
+ "message": "CardDAV server address:"
+ },
+ "pref.UserName": {
+ "message": "User name"
+ },
+ "pref.calendaroptions": {
+ "message": "Calendar options"
+ },
+ "pref.contactoptions": {
+ "message": "Contact options"
+ },
+ "pref.downloadonly": {
+ "message": "Revert local changes (one-way sync)"
+ },
+ "pref.generaloptions": {
+ "message": "General options"
+ },
+ "pref.syncGroups": {
+ "message": "Sync contact groups as Thunderbird lists"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "NOTE: Due to TB bug 1522453, TbSync must add dummy e-mail addresses to all contacts, which are part of groups but do not have an e-mail address associated. These dummy e-mail addresses are not synced to server."
+ },
+ "pref.useCalendarCache": {
+ "message": "Offline Support"
+ },
+ "pref.useCardBook": {
+ "message": "Create CardBook address books instead of Thunderbird's default address books"
+ },
+ "status.401": {
+ "message": "Could not authenticate, check username and password."
+ },
+ "status.403": {
+ "message": "Server rejected connection (forbidden)."
+ },
+ "status.404": {
+ "message": "HTTP Error 404 (requested resource not found)."
+ },
+ "status.500": {
+ "message": "Unknown Server Error (HTTP Error 500)."
+ },
+ "status.503": {
+ "message": "Service unavailable."
+ },
+ "status.caldavservernotfound": {
+ "message": "Could not find a CalDAV server."
+ },
+ "status.carddavservernotfound": {
+ "message": "Could not find a CardDAV server."
+ },
+ "status.gContactSync": {
+ "message": "There is an incompatibility with gContactSync with activated contact group synchronization. Please deactivate one of them as long as the error is not resolved."
+ },
+ "status.info.restored": {
+ "message": "Due to partially missing write permissions, some actions were rejected by the server and reversed locally."
+ },
+ "status.malformed-xml": {
+ "message": "Could not parse XML. Check event log for details."
+ },
+ "status.missing-permission": {
+ "message": "Missing permission: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Could not connect to server."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Non fatal error (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Updating folder list"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Processing acknowledgment of local changes"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Processing remote changes"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Sending local changes"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Requesting folder list"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Waiting for acknowledgment of local changes"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Waiting for remote changes"
+ }
+} \ No newline at end of file
diff --git a/_locales/de/messages.json b/_locales/de/messages.json
new file mode 100644
index 0000000..150ab92
--- /dev/null
+++ b/_locales/de/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Kontakteigenschaften (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "E-Mail Adressen"
+ },
+ "abCard.MiddleName": {
+ "message": "Zweiter Vorname:"
+ },
+ "abCard.Phone": {
+ "message": "Telefonnummern"
+ },
+ "abCard.PrefixName": {
+ "message": "Präfix:"
+ },
+ "abCard.SuffixName": {
+ "message": "Suffix:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Autotelefon"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Handy"
+ },
+ "abCard.emailtypes.description": {
+ "message": "Die erste E-Mailadresse in der Liste wird als primäre E-Mailadresse verwendet."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Privat"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Sonstige"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Video"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Telefon"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Dienstlich"
+ },
+ "acl.add": {
+ "message": "hinzufügen"
+ },
+ "acl.delete": {
+ "message": "löschen"
+ },
+ "acl.modify": {
+ "message": "bearbeiten"
+ },
+ "acl.none": {
+ "message": "keine"
+ },
+ "acl.readonly": {
+ "message": "Serverzugriff nur lesend (verwerfe lokale Änderungen)"
+ },
+ "acl.readwrite": {
+ "message": "Schreibrechte auf Server: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "CalDAV Serveradresse:"
+ },
+ "add.carddavserver": {
+ "message": "CardDAV Serveradresse:"
+ },
+ "add.data.description": {
+ "message": "Bitte geben Sie einen Namen für das neue TbSync-Konto und die Anmeldeinformationen für Ihren Server an:"
+ },
+ "add.data.notes": {
+ "message": "Hinweise:"
+ },
+ "add.data.title": {
+ "message": "Kontoinformationen angeben"
+ },
+ "add.finish.description": {
+ "message": "Die folgenden Einstellungen wurden erfolgreich verifiziert:"
+ },
+ "add.finish.details": {
+ "message": "Klicken Sie auf „Fertigstellen“, um ein neues TbSync-Konto mit diesen Einstellungen anzulegen."
+ },
+ "add.finish.title": {
+ "message": "Kontoerstellung abschließen"
+ },
+ "add.name": {
+ "message": "Kontoname:"
+ },
+ "add.ok": {
+ "message": "Konto hinzufügen"
+ },
+ "add.password": {
+ "message": "Passwort:"
+ },
+ "add.server": {
+ "message": "Serveradresse:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Benutzerdefinierte Konfiguration"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "Die CalDAV und CardDAV Service-Endpunkte können manuell konfiguriert werden."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "Die benötigten CalDAV & CardDAV Service-Endpunkte bzw. die Prinzipal-Adressen sollten Sie bei Ihrem Serviceanbieter erfragen können."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "Wenn Sie eine der beiden Adressen leer lassen, wird der entsprechende Dienst für dieses Konto deaktiviert."
+ },
+ "add.serverprofile.description": {
+ "message": "Bitte wählen Sie eines der verfügbaren Serverprofile aus:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Automatische Konfiguration"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "optionale Angabe"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europa)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "Dies ist eine von Apple eingeführte zusätzliche Sicherheitsebene, sodass Drittanbieter-Clients keinen Zugriff auf Ihr Apple-Konto erhalten."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "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.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Serverprofil auswählen"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Sende RFC6764-Anfrage an „##replace.1##“"
+ },
+ "add.spinner.validating": {
+ "message": "Überprüfe Verbindung zum Server"
+ },
+ "add.title": {
+ "message": "CalDAV & CardDAV Konto hinzufügen"
+ },
+ "add.user": {
+ "message": "Benutzername:"
+ },
+ "autocomplete.HOME": {
+ "message": "privat"
+ },
+ "autocomplete.PREF": {
+ "message": "bevorzugt"
+ },
+ "autocomplete.WORK": {
+ "message": "dienstlich"
+ },
+ "config.custom": {
+ "message": "CalDAV & CardDAV Server Konfiguration"
+ },
+ "defaultname.calendar": {
+ "message": "Kalender"
+ },
+ "defaultname.contacts": {
+ "message": "Kontakte"
+ },
+ "extensionDescription": {
+ "message": "Erweitert TbSync und erlaubt die Synchronisation von CalDAV & CardDAV Konten (Kontakte, Aufgaben und Kalender)."
+ },
+ "extensionName": {
+ "message": "Provider für CalDAV & CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Kontoeinstellungen"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Optionen"
+ },
+ "menu.name": {
+ "message": "CalDAV & CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Kontoname"
+ },
+ "pref.CalDavServer": {
+ "message": "CalDAV Serveradresse:"
+ },
+ "pref.CardDavServer": {
+ "message": "CardDAV Serveradresse:"
+ },
+ "pref.UserName": {
+ "message": "Benutzername"
+ },
+ "pref.calendaroptions": {
+ "message": "Kalender Optionen"
+ },
+ "pref.contactoptions": {
+ "message": "Kontakt Optionen"
+ },
+ "pref.downloadonly": {
+ "message": "Lokale Änderungen verwerfen (one-way sync)"
+ },
+ "pref.generaloptions": {
+ "message": "Allgemeine Optionen"
+ },
+ "pref.syncGroups": {
+ "message": "Kontaktgruppen als Thunderbird-Listen synchronisieren"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "Hinweis: Auf Grund von TB bug 1522453 muss TbSync allen Kontakten eine Dummy-E-Mail-Adresse hinzufügen, die Teil einer Gruppen sind, die jedoch keine E-Mail-Adresse haben. Diese Dummy-E-Mail-Adresse wird nicht mit dem Server synchronisiert."
+ },
+ "pref.useCalendarCache": {
+ "message": "Offline-Unterstützung"
+ },
+ "pref.useCardBook": {
+ "message": "Erstelle CardBook-Adressbücher anstelle der Standard-Adressbücher von Thunderbird"
+ },
+ "status.401": {
+ "message": "Authentifizierung fehlgeschlagen, überprüfen Sie den Benutzernamen und das Passwort."
+ },
+ "status.403": {
+ "message": "Verbindung vom Server abgelehnt (nicht erlaubt)."
+ },
+ "status.404": {
+ "message": "HTTP Fehler 404 (angeforderte Resource nicht gefunden)."
+ },
+ "status.500": {
+ "message": "Unbekannter Server Fehler (HTTP Fehler 500)."
+ },
+ "status.503": {
+ "message": "Service nicht erreichbar."
+ },
+ "status.caldavservernotfound": {
+ "message": "Es wurde kein CalDAV Server gefunden."
+ },
+ "status.carddavservernotfound": {
+ "message": "Es wurde kein CardDAV Server gefunden."
+ },
+ "status.gContactSync": {
+ "message": "Es besteht eine Inkompatibilität mit gContactSync bei aktivierter Synchronisation der Kontaktgruppen. Bitte deaktivieren eines von beiden, solange der Fehler nicht behoben ist."
+ },
+ "status.info.restored": {
+ "message": "Wegen teilweise fehlender Schreibrechte wurden einige Aktionen vom Server zurückgewiesen und lokal rückgängig gemacht."
+ },
+ "status.malformed-xml": {
+ "message": "Antwort enthält fehlerhaftes XML, Sync abgebrochen. Prüfen Sie bitte das Ereignisprotokoll für weitere Details."
+ },
+ "status.missing-permission": {
+ "message": "Fehlende Berechtigung: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Verbindung zum Server fehlgeschlagen."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Ignorierter Fehler (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Verarbeite Ordnerliste"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Verarbeite Bestätigung der lokalen Änderungen"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Verarbeite Serverdaten"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Sende lokale Änderungen"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Sende Anfrage bzgl. Ordnerliste"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Warte auf Bestätigung der lokalen Änderungen"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Warte auf Daten vom Server"
+ }
+} \ No newline at end of file
diff --git a/_locales/en-US/messages.json b/_locales/en-US/messages.json
new file mode 100644
index 0000000..c74516e
--- /dev/null
+++ b/_locales/en-US/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Contact properties (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "E-mail addresses"
+ },
+ "abCard.MiddleName": {
+ "message": "Middle:"
+ },
+ "abCard.Phone": {
+ "message": "Phone numbers"
+ },
+ "abCard.PrefixName": {
+ "message": "Prefix:"
+ },
+ "abCard.SuffixName": {
+ "message": "Suffix:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Car"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Mobile"
+ },
+ "abCard.emailtypes.description": {
+ "message": "The first e-mail address in the list will be used as the primary e-mail address."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Home"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Other"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Video"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Phone"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Work"
+ },
+ "acl.add": {
+ "message": "add"
+ },
+ "acl.delete": {
+ "message": "delete"
+ },
+ "acl.modify": {
+ "message": "modify"
+ },
+ "acl.none": {
+ "message": "none"
+ },
+ "acl.readonly": {
+ "message": "Read-only server access (revert local changes)"
+ },
+ "acl.readwrite": {
+ "message": "Server write permissions: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "CalDAV server address:"
+ },
+ "add.carddavserver": {
+ "message": "CardDAV server address:"
+ },
+ "add.data.description": {
+ "message": "Please provide a friendly name for the new TbSync account and the credentials for your server:"
+ },
+ "add.data.notes": {
+ "message": "Notes:"
+ },
+ "add.data.title": {
+ "message": "Enter account information"
+ },
+ "add.finish.description": {
+ "message": "The following settings have been verified successfully:"
+ },
+ "add.finish.details": {
+ "message": "Click “Finish” to create a new TbSync account with these settings."
+ },
+ "add.finish.title": {
+ "message": "Confirm account creation"
+ },
+ "add.name": {
+ "message": "Account name:"
+ },
+ "add.ok": {
+ "message": "Add account"
+ },
+ "add.password": {
+ "message": "Password:"
+ },
+ "add.server": {
+ "message": "Server URL:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Manual Configuration"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "The CalDAV and CardDAV service endpoints can be configured manually."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "The required CalDAV & CardDAV service endpoints or the so called principal addresses should be provided by your service provider."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "If you leave either address empty, the corresponding service will be disabled for this account."
+ },
+ "add.serverprofile.description": {
+ "message": "Please select one of the available server profiles:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Automatic Configuration"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "optional"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europe)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "This is a security layer enforced by Apple, so that 3rd party clients do not gain access to your Apple account."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage."
+ },
+ "add.serverprofile.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Select a server profile"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Sending RFC6764 request to “##replace.1##”"
+ },
+ "add.spinner.validating": {
+ "message": "Verifying connection to server"
+ },
+ "add.title": {
+ "message": "Adding a CalDAV & CardDAV account to TbSync"
+ },
+ "add.user": {
+ "message": "User name:"
+ },
+ "autocomplete.HOME": {
+ "message": "private"
+ },
+ "autocomplete.PREF": {
+ "message": "preferred"
+ },
+ "autocomplete.WORK": {
+ "message": "business"
+ },
+ "config.custom": {
+ "message": "CalDAV & CardDAV server configuration"
+ },
+ "defaultname.calendar": {
+ "message": "Calendar"
+ },
+ "defaultname.contacts": {
+ "message": "Contacts"
+ },
+ "extensionDescription": {
+ "message": "Add sync support for CalDAV & CardDAV accounts to TbSync."
+ },
+ "extensionName": {
+ "message": "Provider for CalDAV & CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Account settings"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Options"
+ },
+ "menu.name": {
+ "message": "CalDAV & CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Account name"
+ },
+ "pref.CalDavServer": {
+ "message": "CalDAV server address:"
+ },
+ "pref.CardDavServer": {
+ "message": "CardDAV server address:"
+ },
+ "pref.UserName": {
+ "message": "User name"
+ },
+ "pref.calendaroptions": {
+ "message": "Calendar options"
+ },
+ "pref.contactoptions": {
+ "message": "Contact options"
+ },
+ "pref.downloadonly": {
+ "message": "Revert local changes (one-way sync)"
+ },
+ "pref.generaloptions": {
+ "message": "General options"
+ },
+ "pref.syncGroups": {
+ "message": "Sync contact groups as Thunderbird lists"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "NOTE: Due to TB bug 1522453, TbSync must add dummy e-mail addresses to all contacts, which are part of groups but do not have an e-mail address associated. These dummy e-mail addresses are not synced to server."
+ },
+ "pref.useCalendarCache": {
+ "message": "Offline Support"
+ },
+ "pref.useCardBook": {
+ "message": "Create CardBook address books instead of Thunderbird's default address books"
+ },
+ "status.401": {
+ "message": "Could not authenticate, check username and password."
+ },
+ "status.403": {
+ "message": "Server rejected connection (forbidden)."
+ },
+ "status.404": {
+ "message": "HTTP Error 404 (requested resource not found)."
+ },
+ "status.500": {
+ "message": "Unknown Server Error (HTTP Error 500)."
+ },
+ "status.503": {
+ "message": "Service unavailable."
+ },
+ "status.caldavservernotfound": {
+ "message": "Could not find a CalDAV server."
+ },
+ "status.carddavservernotfound": {
+ "message": "Could not find a CardDAV server."
+ },
+ "status.gContactSync": {
+ "message": "There is an incompatibility with gContactSync with activated contact group synchronization. Please deactivate one of them as long as the error is not resolved."
+ },
+ "status.info.restored": {
+ "message": "Due to partially missing write permissions, some actions were rejected by the server and reversed locally."
+ },
+ "status.malformed-xml": {
+ "message": "Could not parse XML. Check event log for details."
+ },
+ "status.missing-permission": {
+ "message": "Missing permission: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Could not connect to server."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Non fatal error (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Updating folder list"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Processing acknowledgment of local changes"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Processing remote changes"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Sending local changes"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Requesting folder list"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Waiting for acknowledgment of local changes"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Waiting for remote changes"
+ }
+} \ No newline at end of file
diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json
new file mode 100644
index 0000000..9900f1a
--- /dev/null
+++ b/_locales/fr/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Propriété du contact (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Adresse de courriel"
+ },
+ "abCard.MiddleName": {
+ "message": "Second prénom:"
+ },
+ "abCard.Phone": {
+ "message": "Numéros de téléphone"
+ },
+ "abCard.PrefixName": {
+ "message": "Préfix:"
+ },
+ "abCard.SuffixName": {
+ "message": "Suffixe:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Voiture"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Portable"
+ },
+ "abCard.emailtypes.description": {
+ "message": "La première adresse de courriel de la liste sera utilisée comme adresse principale."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Domicile"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Autre"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Téléavertisseur"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Vidéoconférence"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Téléphone"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Travail"
+ },
+ "acl.add": {
+ "message": "ajouter"
+ },
+ "acl.delete": {
+ "message": "supprimer"
+ },
+ "acl.modify": {
+ "message": "modifier"
+ },
+ "acl.none": {
+ "message": "aucun"
+ },
+ "acl.readonly": {
+ "message": "Accès au serveur en lecture seule (annule les modification locales)"
+ },
+ "acl.readwrite": {
+ "message": "Droits en écriture sur le serveur: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "Adresse du serveur CalDAV:"
+ },
+ "add.carddavserver": {
+ "message": "Adresse du serveur CardDAV:"
+ },
+ "add.data.description": {
+ "message": "Veuillez fournir un nom convivial pour le nouveau compte TbSync, ainsi que le couple identifiant/mot de passe d'accès à votre serveur:"
+ },
+ "add.data.notes": {
+ "message": "Notes:"
+ },
+ "add.data.title": {
+ "message": "Information de compte"
+ },
+ "add.finish.description": {
+ "message": "Les paramètres suivants ont été vérifiés avec succès :"
+ },
+ "add.finish.details": {
+ "message": "Cliquez sur « Terminer » pour créer un nouveau compte TbSync avec ces paramètres."
+ },
+ "add.finish.title": {
+ "message": "Confirmer la création d'un compte"
+ },
+ "add.name": {
+ "message": "Nom du compte:"
+ },
+ "add.ok": {
+ "message": "Ajouter un compte"
+ },
+ "add.password": {
+ "message": "Mot de passe:"
+ },
+ "add.server": {
+ "message": "URL du serveur:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Configuration manuelle"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "Les URL d'accès aux services CalDAV et CardDAV peuvent être configurées manuellement."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "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": {
+ "message": "Si vous laissez le champ d'adresse vide, le service correspondant sera désactivé pour ce compte."
+ },
+ "add.serverprofile.description": {
+ "message": "Veuillez sélectionner un des profils de serveur disponibles:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Configuration automatique"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "facultatif"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europe)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "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.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "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.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Sélection d'un profil de serveur"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "Le mot de passe demandé est un mot de passe spécifique à l'application pour TbSync que vous pouvez créer sur votre portail Web Yahoo! Ce n'est pas votre mot de passe standard Yahoo!"
+ },
+ "add.spinner.query": {
+ "message": "Envoi d'une demande RFC6764 à «##replace.1## »"
+ },
+ "add.spinner.validating": {
+ "message": "Vérification de la connexion au serveur"
+ },
+ "add.title": {
+ "message": "Ajout d'un compte CalDAV & CardDAV à TbSync"
+ },
+ "add.user": {
+ "message": "Nom d'utilisateur:"
+ },
+ "autocomplete.HOME": {
+ "message": "privé"
+ },
+ "autocomplete.PREF": {
+ "message": "favori"
+ },
+ "autocomplete.WORK": {
+ "message": "travail"
+ },
+ "config.custom": {
+ "message": "Configuration des serveurs CalDAV et CardDAV"
+ },
+ "defaultname.calendar": {
+ "message": "Agenda"
+ },
+ "defaultname.contacts": {
+ "message": "Contacts"
+ },
+ "extensionDescription": {
+ "message": "Ajoute à TbSync la prise en charge des comptes CalDAV & CardDAV."
+ },
+ "extensionName": {
+ "message": "Provider pour CalDAV & CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/59#issuecomment-459685281"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Paramètres du compte"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Paramètres"
+ },
+ "menu.name": {
+ "message": "CalDAV & CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Nom du compte"
+ },
+ "pref.CalDavServer": {
+ "message": "Adresse du serveur CalDAV:"
+ },
+ "pref.CardDavServer": {
+ "message": "Adresse du serveur CardDAV:"
+ },
+ "pref.UserName": {
+ "message": "Nom d'utilisateur"
+ },
+ "pref.calendaroptions": {
+ "message": "Options de calendrier"
+ },
+ "pref.contactoptions": {
+ "message": "Options des contacts"
+ },
+ "pref.downloadonly": {
+ "message": "Annuler les modifications locales (synchronisation à sens unique)"
+ },
+ "pref.generaloptions": {
+ "message": "Options générales"
+ },
+ "pref.syncGroups": {
+ "message": "Synchroniser les groupes de contacts comme des listes Thunderbird lists"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "NOTE: A cause du bug Thunberbird 1522453, TbSync est obligé d'ajouter une adresse fictive à tous les contacts lorsque ceux-ci sont membres d'un groupe mais qu'aucune adresse e-mail ne leur est associée. Ces adresses fictives ne sont pas syncrhonisées vers le serveur, mais reste locale à Thunderbird."
+ },
+ "pref.useCalendarCache": {
+ "message": "Prise en charge du mode hors-ligne"
+ },
+ "pref.useCardBook": {
+ "message": "Créer un carnet d'adresse dans CardBook plutôt que dans le carnet d'adresse standard de Thunderbird"
+ },
+ "status.401": {
+ "message": "L'authentification a échoué. Veuillez vérifier le couple nom d'utilisateur/mot de passe."
+ },
+ "status.403": {
+ "message": "Le serveur a refusé la connexion (accès interdit)."
+ },
+ "status.404": {
+ "message": "HTTP Erreur 404 (la ressource demandée n'a pas été trouvée)."
+ },
+ "status.500": {
+ "message": "Erreur serveur inconnue(Erreur HTTP 500)."
+ },
+ "status.503": {
+ "message": "Service indisponible."
+ },
+ "status.caldavservernotfound": {
+ "message": "Impossible de trouver un serveur CalDAV."
+ },
+ "status.carddavservernotfound": {
+ "message": "Impossible de trouver un serveur CardDAV."
+ },
+ "status.gContactSync": {
+ "message": "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."
+ },
+ "status.info.restored": {
+ "message": "À cause de droits en écriture partiellement insuffisants, certaines actions ont été refusées par le serveur et annulées localement."
+ },
+ "status.malformed-xml": {
+ "message": "Analyse de l'XML impossible. Veuillez consulter le journal de débogage pour plus de détails. "
+ },
+ "status.missing-permission": {
+ "message": "Droit manquant: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "La connexion au serveur a échoué."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Erreur non critique (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Mise à jour de la liste des dossiers"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Traitement de la confirmation de réception des modifications locales"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "En cours de traitement des modifications distantes"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Envoi des modifications locales"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Demande de la liste des dossiers"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "En attente de la confirmation de réception des modifications locales"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "En attente des modifications distantes"
+ }
+} \ No newline at end of file
diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json
new file mode 100644
index 0000000..cf6002b
--- /dev/null
+++ b/_locales/hu/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Kapcsolati tulajdonságok (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Email címek:"
+ },
+ "abCard.MiddleName": {
+ "message": "Egyéb név:"
+ },
+ "abCard.Phone": {
+ "message": "Telefonszámok"
+ },
+ "abCard.PrefixName": {
+ "message": "előtagja:"
+ },
+ "abCard.SuffixName": {
+ "message": "képző:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "autó"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "mozgó"
+ },
+ "abCard.emailtypes.description": {
+ "message": "A lista első e-mail címe az elsődleges e-mail cím."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "itthon"
+ },
+ "abCard.emailtypes.other": {
+ "message": "más"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "videó"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "telefon"
+ },
+ "abCard.emailtypes.work": {
+ "message": "üzleti"
+ },
+ "acl.add": {
+ "message": "hozzáad"
+ },
+ "acl.delete": {
+ "message": "töröl"
+ },
+ "acl.modify": {
+ "message": "módosít"
+ },
+ "acl.none": {
+ "message": "egyik sem"
+ },
+ "acl.readonly": {
+ "message": "csak olvasható"
+ },
+ "acl.readwrite": {
+ "message": "írásjog: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "CardDAV kiszolgáló cím:"
+ },
+ "add.carddavserver": {
+ "message": "CardDAV kiszolgáló cím:"
+ },
+ "add.data.description": {
+ "message": "Kérjük, adjon meg egy becenévet az új Thunderbird-összehangolás fióknak és a hitelesítő adatoknak a kiszolgáló számára:"
+ },
+ "add.data.notes": {
+ "message": "Megjegyzések:"
+ },
+ "add.data.title": {
+ "message": "Fiókadatok megadása"
+ },
+ "add.finish.description": {
+ "message": "?? The following settings have been verified successfully: ??"
+ },
+ "add.finish.details": {
+ "message": "?? Click „Finish“ to create a new TbSync account with these settings. ??"
+ },
+ "add.finish.title": {
+ "message": "?? Confirm account creation ??"
+ },
+ "add.name": {
+ "message": "Fiók neve:"
+ },
+ "add.ok": {
+ "message": "Fiók hozzáadása"
+ },
+ "add.password": {
+ "message": "Jelszó:"
+ },
+ "add.server": {
+ "message": "Kiszolgáló URL:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Kézi beállítás"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "A CalDAV és a CardDAV szolgáltatás végpontjai kézzel beállításhatók."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "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": {
+ "message": "Ha a két címet üresen hagyja, akkor a megfelelő szolgáltatás le lesz tiltva erre a fiókra."
+ },
+ "add.serverprofile.description": {
+ "message": "Kérjük, válasszon egyet a rendelkezésre álló kiszolgálóprofilok közül:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Önműködő konfigurálás"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "választható"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (Amerikai egyesült államok)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Európa)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "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.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage."
+ },
+ "add.serverprofile.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Kiszolgálóprofil kiválasztása"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "RFC6764 kérés küldése a(z) „##replace.1##” címre"
+ },
+ "add.spinner.validating": {
+ "message": "Ellenőrizze a kapcsolatot a szerverrel"
+ },
+ "add.title": {
+ "message": "CalDAV és CardDAV fiók hozzáadása a Thunderbird-összehangoláshoz"
+ },
+ "add.user": {
+ "message": "Felhasználói név:"
+ },
+ "autocomplete.HOME": {
+ "message": "magán"
+ },
+ "autocomplete.PREF": {
+ "message": "előnyben részesített"
+ },
+ "autocomplete.WORK": {
+ "message": "üzlet"
+ },
+ "config.custom": {
+ "message": "A CalDAV és CardDAV kiszolgáló beállításai"
+ },
+ "defaultname.calendar": {
+ "message": "naptár"
+ },
+ "defaultname.contacts": {
+ "message": "kapcsolatok"
+ },
+ "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)."
+ },
+ "extensionName": {
+ "message": "A CalDAV és CardDAV szolgáltató"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Fiók beállításai"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Beállítások"
+ },
+ "menu.name": {
+ "message": "CalDAV és CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Fiók neve:"
+ },
+ "pref.CalDavServer": {
+ "message": "CardDAV kiszolgáló cím:"
+ },
+ "pref.CardDavServer": {
+ "message": "CardDAV kiszolgáló cím:"
+ },
+ "pref.UserName": {
+ "message": "Felhasználói név"
+ },
+ "pref.calendaroptions": {
+ "message": "Naptár beállításai"
+ },
+ "pref.contactoptions": {
+ "message": "Névjegyzék beállításai"
+ },
+ "pref.downloadonly": {
+ "message": "A helyi változások visszaállítása (egyirányú összehangolás)"
+ },
+ "pref.generaloptions": {
+ "message": "Általános beállításai"
+ },
+ "pref.syncGroups": {
+ "message": "Szinkronizálja a névjegycsoportokat Thunderbird listákként"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "MEGJEGYZÉS: A 1522453-as TB-bug miatt a TbSync-nek dummy e-mail címeket kell hozzáadnia minden kapcsolathoz, amelyek csoportok részét képezik, de nem rendelkeznek e-mail címmel. Ezek a dummy e-mail címek nem szinkronizálódnak a szerverrel."
+ },
+ "pref.useCalendarCache": {
+ "message": "Kapcsolat nélküli munka"
+ },
+ "pref.useCardBook": {
+ "message": "A Thunderbird alapértelmezett címjegyzéke helyett hozzon létre CardBook címjegyzékeket"
+ },
+ "status.401": {
+ "message": "Nem sikerült hitelesíteni, ellenőrizni a felhasználónevet és a jelszót."
+ },
+ "status.403": {
+ "message": "403-as HTTP hibakód (hozzáférés megtagadva/tiltott)."
+ },
+ "status.404": {
+ "message": "404-es HTTP hibakód (nem található)."
+ },
+ "status.500": {
+ "message": "500-as hibakód (belső kiszolgálóhiba)."
+ },
+ "status.503": {
+ "message": "503-as hibakód (a szolgáltatás nem érhető el)."
+ },
+ "status.caldavservernotfound": {
+ "message": "Nem található CalDAV szerver."
+ },
+ "status.carddavservernotfound": {
+ "message": "Nem található CardDAV szerver."
+ },
+ "status.gContactSync": {
+ "message": "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."
+ },
+ "status.info.restored": {
+ "message": "A hiányzó írási jogosultságok miatt egyes műveleteket a szerver elutasított, és helyileg visszafordított."
+ },
+ "status.malformed-xml": {
+ "message": "Nem sikerült elemezni az XML-t. Ellenőrizze az eseménynaplót a részletekért."
+ },
+ "status.missing-permission": {
+ "message": "Hiányzó engedély: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Nem tudott csatlakozni a kiszolgálóhoz."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Kihagyta a hibát (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Mappalisták frissítése"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Helyi változtatások igazolása feldolgozása"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "A távoli változtatások feldolgozása"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Helyi változtatások küldése"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Mappalisták lekérése"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Várakozás a helyi változtatások igazolásra"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Várakozás a távoli változtatások"
+ }
+} \ No newline at end of file
diff --git a/_locales/it/messages.json b/_locales/it/messages.json
new file mode 100644
index 0000000..6b31b53
--- /dev/null
+++ b/_locales/it/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Proprietà del contatto (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Indirizzi email:"
+ },
+ "abCard.MiddleName": {
+ "message": "Secondo nome:"
+ },
+ "abCard.Phone": {
+ "message": "Numeri di telefono"
+ },
+ "abCard.PrefixName": {
+ "message": "Prefisso:"
+ },
+ "abCard.SuffixName": {
+ "message": "Suffisso:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "auto"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "mobile"
+ },
+ "abCard.emailtypes.description": {
+ "message": "Il primo indirizzo e-mail nell'elenco viene utilizzato come indirizzo di posta elettronica principale."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "privatamente"
+ },
+ "abCard.emailtypes.other": {
+ "message": "altro"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "cercapersone"
+ },
+ "abCard.emailtypes.video": {
+ "message": "video"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "telefono"
+ },
+ "abCard.emailtypes.work": {
+ "message": "affari"
+ },
+ "acl.add": {
+ "message": "inserisci"
+ },
+ "acl.delete": {
+ "message": "elimina"
+ },
+ "acl.modify": {
+ "message": "modificare"
+ },
+ "acl.none": {
+ "message": "nessuna"
+ },
+ "acl.readonly": {
+ "message": "sola lettura"
+ },
+ "acl.readwrite": {
+ "message": "Scrivi permesso: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "Indirizzo server CalDAV:"
+ },
+ "add.carddavserver": {
+ "message": "Indirizzo server CardDAV:"
+ },
+ "add.data.description": {
+ "message": "Fornisci un nome descrittivo per il nuovo account TbSync e le credenziali del server:"
+ },
+ "add.data.notes": {
+ "message": "Note:"
+ },
+ "add.data.title": {
+ "message": "Immetti informazioni account"
+ },
+ "add.finish.description": {
+ "message": "?? The following settings have been verified successfully: ??"
+ },
+ "add.finish.details": {
+ "message": "?? Click „Finish“ to create a new TbSync account with these settings. ??"
+ },
+ "add.finish.title": {
+ "message": "?? Confirm account creation ??"
+ },
+ "add.name": {
+ "message": "Nome account:"
+ },
+ "add.ok": {
+ "message": "Aggiungi account"
+ },
+ "add.password": {
+ "message": "Password:"
+ },
+ "add.server": {
+ "message": "Indirizzo server:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Configurazione personalizzata"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "Gli endpoint servizio CalDAV e CardDAV possono essere configurati manualmente."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "Gli endpoint servizio CalDAV e CardDAV richiesti o i cosiddetti indirizzi principali dovrebbero essere forniti dal tuo provider di servizi."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "Se si lascia un indirizzo vuoto, il servizio corrispondente sarà disabilitato per quest'account."
+ },
+ "add.serverprofile.description": {
+ "message": "Seleziona uno dei profili server disponibili:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Configurazione automatica"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "facoltativo"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "fruux è un servizio che sincronizza contatti, calendari e attività. È alimentato dalla società dietro sabre/dav e ha sede in Germania."
+ },
+ "add.serverprofile.gmx.com": {
+ "message": "GMX.com (Stati Uniti d'America)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europa)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "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.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "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.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Seleziona un profilo server"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Invio richiesta RFC6764 a '##replace.1##' in corso"
+ },
+ "add.spinner.validating": {
+ "message": "Controlla la connessione al server"
+ },
+ "add.title": {
+ "message": "Aggiunta account CalDAV e CardDAV a TbSync"
+ },
+ "add.user": {
+ "message": "Nome utente:"
+ },
+ "autocomplete.HOME": {
+ "message": "personale"
+ },
+ "autocomplete.PREF": {
+ "message": "preferito"
+ },
+ "autocomplete.WORK": {
+ "message": "lavoro"
+ },
+ "config.custom": {
+ "message": "Configurazione server CalDAV & CardDAV"
+ },
+ "defaultname.calendar": {
+ "message": "calendario"
+ },
+ "defaultname.contacts": {
+ "message": "contatti"
+ },
+ "extensionDescription": {
+ "message": "Aggiunge il supporto per la sincronizzazione degli account CalDAV e CardDAV a TbSync."
+ },
+ "extensionName": {
+ "message": "Provider CalDAV e CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Impostazioni account"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Opzioni"
+ },
+ "menu.name": {
+ "message": "CalDAV e CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Nome account"
+ },
+ "pref.CalDavServer": {
+ "message": "Indirizzo server CalDAV:"
+ },
+ "pref.CardDavServer": {
+ "message": "Indirizzo server CardDAV:"
+ },
+ "pref.UserName": {
+ "message": "Nome utente"
+ },
+ "pref.calendaroptions": {
+ "message": "Opzioni calendario"
+ },
+ "pref.contactoptions": {
+ "message": "Opzioni contatto"
+ },
+ "pref.downloadonly": {
+ "message": "Annulla modifiche locali (sincronizzazione a una via)"
+ },
+ "pref.generaloptions": {
+ "message": "Opzioni generali"
+ },
+ "pref.syncGroups": {
+ "message": "Sincronizza i gruppi di contatti come elenchi di Thunderbird"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "NOTA: a causa del bug TB 1522453, TbSync deve aggiungere indirizzi email fittizi a tutti i contatti, che fanno parte di gruppi ma non hanno un indirizzo email associato. Questi indirizzi email fittizi non sono sincronizzati con il server."
+ },
+ "pref.useCalendarCache": {
+ "message": "Supporto non in linea"
+ },
+ "pref.useCardBook": {
+ "message": "Crea rubriche di CardBook al posto delle rubriche di default di Thunderbird"
+ },
+ "status.401": {
+ "message": "Impossibile autenticarsi, controllare nome utente e password."
+ },
+ "status.403": {
+ "message": "Il server ha rifiutato la connessione (vietato)."
+ },
+ "status.404": {
+ "message": "Errore HTTP 404 (risorsa richiesta non trovata)."
+ },
+ "status.500": {
+ "message": "Errore server sconosciuto (errore HTTP 500)."
+ },
+ "status.503": {
+ "message": "Servizio non disponibile."
+ },
+ "status.caldavservernotfound": {
+ "message": "Impossibile trovare un server CalDAV."
+ },
+ "status.carddavservernotfound": {
+ "message": "Impossibile trovare un server CardDAV."
+ },
+ "status.gContactSync": {
+ "message": "Il software è incompatibile con gContactSync con la sincronizzazione gruppi contatti attivata. Disattivarne uno finché l'errore non sarà risolto."
+ },
+ "status.info.restored": {
+ "message": "A causa della mancanza di permessi di scrittura, alcune azioni sono state respinte dal server e annullate localmente."
+ },
+ "status.malformed-xml": {
+ "message": "Impossibile analizzare XML. Controllare il registro eventi per i dettagli."
+ },
+ "status.missing-permission": {
+ "message": "Permesso mancante: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Impossibile connettersi al server."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Errore saltato (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Aggiornamento elenco cartelle in corso"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Elaborazione riconoscimento modifiche locali in corso"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Elaborazione modifiche remote in corso"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Invio modifiche locali in corso"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Richiesta elenco cartelle in corso"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "In attesa del riconoscimento delle modifiche locali"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "In attesa delle modifiche remote"
+ }
+} \ No newline at end of file
diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json
new file mode 100644
index 0000000..c74516e
--- /dev/null
+++ b/_locales/ja/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Contact properties (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "E-mail addresses"
+ },
+ "abCard.MiddleName": {
+ "message": "Middle:"
+ },
+ "abCard.Phone": {
+ "message": "Phone numbers"
+ },
+ "abCard.PrefixName": {
+ "message": "Prefix:"
+ },
+ "abCard.SuffixName": {
+ "message": "Suffix:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Car"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Mobile"
+ },
+ "abCard.emailtypes.description": {
+ "message": "The first e-mail address in the list will be used as the primary e-mail address."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Home"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Other"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Video"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Phone"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Work"
+ },
+ "acl.add": {
+ "message": "add"
+ },
+ "acl.delete": {
+ "message": "delete"
+ },
+ "acl.modify": {
+ "message": "modify"
+ },
+ "acl.none": {
+ "message": "none"
+ },
+ "acl.readonly": {
+ "message": "Read-only server access (revert local changes)"
+ },
+ "acl.readwrite": {
+ "message": "Server write permissions: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "CalDAV server address:"
+ },
+ "add.carddavserver": {
+ "message": "CardDAV server address:"
+ },
+ "add.data.description": {
+ "message": "Please provide a friendly name for the new TbSync account and the credentials for your server:"
+ },
+ "add.data.notes": {
+ "message": "Notes:"
+ },
+ "add.data.title": {
+ "message": "Enter account information"
+ },
+ "add.finish.description": {
+ "message": "The following settings have been verified successfully:"
+ },
+ "add.finish.details": {
+ "message": "Click “Finish” to create a new TbSync account with these settings."
+ },
+ "add.finish.title": {
+ "message": "Confirm account creation"
+ },
+ "add.name": {
+ "message": "Account name:"
+ },
+ "add.ok": {
+ "message": "Add account"
+ },
+ "add.password": {
+ "message": "Password:"
+ },
+ "add.server": {
+ "message": "Server URL:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Manual Configuration"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "The CalDAV and CardDAV service endpoints can be configured manually."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "The required CalDAV & CardDAV service endpoints or the so called principal addresses should be provided by your service provider."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "If you leave either address empty, the corresponding service will be disabled for this account."
+ },
+ "add.serverprofile.description": {
+ "message": "Please select one of the available server profiles:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Automatic Configuration"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "optional"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europe)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "This is a security layer enforced by Apple, so that 3rd party clients do not gain access to your Apple account."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage."
+ },
+ "add.serverprofile.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Select a server profile"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Sending RFC6764 request to “##replace.1##”"
+ },
+ "add.spinner.validating": {
+ "message": "Verifying connection to server"
+ },
+ "add.title": {
+ "message": "Adding a CalDAV & CardDAV account to TbSync"
+ },
+ "add.user": {
+ "message": "User name:"
+ },
+ "autocomplete.HOME": {
+ "message": "private"
+ },
+ "autocomplete.PREF": {
+ "message": "preferred"
+ },
+ "autocomplete.WORK": {
+ "message": "business"
+ },
+ "config.custom": {
+ "message": "CalDAV & CardDAV server configuration"
+ },
+ "defaultname.calendar": {
+ "message": "Calendar"
+ },
+ "defaultname.contacts": {
+ "message": "Contacts"
+ },
+ "extensionDescription": {
+ "message": "Add sync support for CalDAV & CardDAV accounts to TbSync."
+ },
+ "extensionName": {
+ "message": "Provider for CalDAV & CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Account settings"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Options"
+ },
+ "menu.name": {
+ "message": "CalDAV & CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Account name"
+ },
+ "pref.CalDavServer": {
+ "message": "CalDAV server address:"
+ },
+ "pref.CardDavServer": {
+ "message": "CardDAV server address:"
+ },
+ "pref.UserName": {
+ "message": "User name"
+ },
+ "pref.calendaroptions": {
+ "message": "Calendar options"
+ },
+ "pref.contactoptions": {
+ "message": "Contact options"
+ },
+ "pref.downloadonly": {
+ "message": "Revert local changes (one-way sync)"
+ },
+ "pref.generaloptions": {
+ "message": "General options"
+ },
+ "pref.syncGroups": {
+ "message": "Sync contact groups as Thunderbird lists"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "NOTE: Due to TB bug 1522453, TbSync must add dummy e-mail addresses to all contacts, which are part of groups but do not have an e-mail address associated. These dummy e-mail addresses are not synced to server."
+ },
+ "pref.useCalendarCache": {
+ "message": "Offline Support"
+ },
+ "pref.useCardBook": {
+ "message": "Create CardBook address books instead of Thunderbird's default address books"
+ },
+ "status.401": {
+ "message": "Could not authenticate, check username and password."
+ },
+ "status.403": {
+ "message": "Server rejected connection (forbidden)."
+ },
+ "status.404": {
+ "message": "HTTP Error 404 (requested resource not found)."
+ },
+ "status.500": {
+ "message": "Unknown Server Error (HTTP Error 500)."
+ },
+ "status.503": {
+ "message": "Service unavailable."
+ },
+ "status.caldavservernotfound": {
+ "message": "Could not find a CalDAV server."
+ },
+ "status.carddavservernotfound": {
+ "message": "Could not find a CardDAV server."
+ },
+ "status.gContactSync": {
+ "message": "There is an incompatibility with gContactSync with activated contact group synchronization. Please deactivate one of them as long as the error is not resolved."
+ },
+ "status.info.restored": {
+ "message": "Due to partially missing write permissions, some actions were rejected by the server and reversed locally."
+ },
+ "status.malformed-xml": {
+ "message": "Could not parse XML. Check event log for details."
+ },
+ "status.missing-permission": {
+ "message": "Missing permission: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Could not connect to server."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Non fatal error (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Updating folder list"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Processing acknowledgment of local changes"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Processing remote changes"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Sending local changes"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Requesting folder list"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Waiting for acknowledgment of local changes"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Waiting for remote changes"
+ }
+} \ No newline at end of file
diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json
new file mode 100644
index 0000000..7cd2cf2
--- /dev/null
+++ b/_locales/pl/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Właściwości kontaktu (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Adresy e-mail"
+ },
+ "abCard.MiddleName": {
+ "message": "Drugie imię:"
+ },
+ "abCard.Phone": {
+ "message": "Numery telefonów"
+ },
+ "abCard.PrefixName": {
+ "message": "Przedrostek:"
+ },
+ "abCard.SuffixName": {
+ "message": "Przyrostek:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "Pojazd"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "Komórka"
+ },
+ "abCard.emailtypes.description": {
+ "message": "Pierwszy adres e-mail z listy będzie używany jako podstawowy adres e-mail."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "Faks"
+ },
+ "abCard.emailtypes.home": {
+ "message": "Dom"
+ },
+ "abCard.emailtypes.other": {
+ "message": "Inny"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "Pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "Film"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "Telefon"
+ },
+ "abCard.emailtypes.work": {
+ "message": "Praca"
+ },
+ "acl.add": {
+ "message": "dodaj"
+ },
+ "acl.delete": {
+ "message": "usuń"
+ },
+ "acl.modify": {
+ "message": "zmień"
+ },
+ "acl.none": {
+ "message": "żaden"
+ },
+ "acl.readonly": {
+ "message": "Dostęp do serwera tylko do odczytu (cofnij zmiany lokalne)"
+ },
+ "acl.readwrite": {
+ "message": "Uprawnienia do zapisu na serwerze: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "Adres serwera CalDAV:"
+ },
+ "add.carddavserver": {
+ "message": "Adres serwera CardDAV:"
+ },
+ "add.data.description": {
+ "message": "Podaj przyjazną nazwę nowego konta TbSync i dane logowania dla Twojego serwera:"
+ },
+ "add.data.notes": {
+ "message": "Notatki:"
+ },
+ "add.data.title": {
+ "message": "Wprowadź informacje konta"
+ },
+ "add.finish.description": {
+ "message": "Następujące ustawienia zostały pomyślnie zweryfikowane:"
+ },
+ "add.finish.details": {
+ "message": "Kliknij \"Zakończ\", aby utworzyć nowe konto TbSync z tymi ustawieniami."
+ },
+ "add.finish.title": {
+ "message": "Potwierdź utworzenie konta"
+ },
+ "add.name": {
+ "message": "Nazwa konta:"
+ },
+ "add.ok": {
+ "message": "Dodaj konto"
+ },
+ "add.password": {
+ "message": "Hasło:"
+ },
+ "add.server": {
+ "message": "URL serwera:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Ręczna Konfiguracja"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "Punkty końcowe usługi CalDAV i CardDAV można skonfigurować ręcznie."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "Wymagane punkty końcowe usługi CalDAV i CardDAV lub tak zwane adresy główne powinien podać dostawca usługi."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "Jeśli którykolwiek z adresów pozostanie pusty, odpowiednia usługa zostanie wyłączona dla tego konta."
+ },
+ "add.serverprofile.description": {
+ "message": "Wybierz jeden z dostępnych profili serwera:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Automatyczna Konfiguracja"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "Dla automatycznego wykrywania punktów końcowych usługi CalDAV i CardDAV, wprowadź swoje poświadczenia i nazwę hosta swojego serwera (np. 'cloud.myserver.de')."
+ },
+ "add.serverprofile.discovery.details2": {
+ "message": "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": {
+ "message": "opcjonalnie"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (USA)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europe)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "Google stosuje nowoczesną metodę uwierzytelniania i 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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "Żą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": {
+ "message": "Jest to warstwa bezpieczeństwa wymuszona przez Apple, aby klienci zewnętrzni nie uzyskali dostępu do Twojego konta Apple."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "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.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Wybierz profil serwera"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "Żą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.spinner.query": {
+ "message": "Wysyłanie żądania RFC6764 do “##replace.1##”"
+ },
+ "add.spinner.validating": {
+ "message": "Weryfikowanie połączenia z serwerem"
+ },
+ "add.title": {
+ "message": "Dodawanie konta CalDAV i CardDAV do TbSync"
+ },
+ "add.user": {
+ "message": "Nazwa użytkownika:"
+ },
+ "autocomplete.HOME": {
+ "message": "prywatny"
+ },
+ "autocomplete.PREF": {
+ "message": "preferowany"
+ },
+ "autocomplete.WORK": {
+ "message": "służbowy"
+ },
+ "config.custom": {
+ "message": "Konfiguracja serwera CalDAV i CardDAV"
+ },
+ "defaultname.calendar": {
+ "message": "Kalendarz"
+ },
+ "defaultname.contacts": {
+ "message": "Kontakty"
+ },
+ "extensionDescription": {
+ "message": "Dodaj do TbSync wsparcie synchronizacji dla CalDAV i CardDAV."
+ },
+ "extensionName": {
+ "message": "Dostawca dla CalDAV i CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Ustawienia konta"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Opcje"
+ },
+ "menu.name": {
+ "message": "CalDAV i CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Nazwa konta"
+ },
+ "pref.CalDavServer": {
+ "message": "Adres serwera CalDAV:"
+ },
+ "pref.CardDavServer": {
+ "message": "Adres serwera CardDAV:"
+ },
+ "pref.UserName": {
+ "message": "Nazwa użytkownika"
+ },
+ "pref.calendaroptions": {
+ "message": "Opcje kalendarza"
+ },
+ "pref.contactoptions": {
+ "message": "Opcje kontaktu"
+ },
+ "pref.downloadonly": {
+ "message": "Cofnij zmiany lokalne (synchronizacja jednokierunkowa)"
+ },
+ "pref.generaloptions": {
+ "message": "Opcje ogólne"
+ },
+ "pref.syncGroups": {
+ "message": "Synchronizuj grupy kontaktów jako listy Thunderbird"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "UWAGA: Z powodu błędu TB 1522453, TbSync musi dodać fikcyjne adresy e-mail do wszystkich kontaktów, które są częścią grup, ale nie mają powiązanego adresu e-mail. Te fałszywe adresy e-mail nie są synchronizowane z serwerem."
+ },
+ "pref.useCalendarCache": {
+ "message": "Wsparcie Offline"
+ },
+ "pref.useCardBook": {
+ "message": "Utwórz książki adresowe CardBook zamiast domyślnych książek adresowych Thunderbirda"
+ },
+ "status.401": {
+ "message": "Nie można uwierzytelnić, sprawdź nazwę użytkownika i hasło."
+ },
+ "status.403": {
+ "message": "Serwer odrzucił połączenie (zabronione)."
+ },
+ "status.404": {
+ "message": "Błąd HTTP 404 (nie znaleziono żądanego zasobu)."
+ },
+ "status.500": {
+ "message": "Nieznany błąd serwera (błąd HTTP 500)."
+ },
+ "status.503": {
+ "message": "Usługa niedostępna."
+ },
+ "status.caldavservernotfound": {
+ "message": "Nie można znaleźć serwera CalDAV."
+ },
+ "status.carddavservernotfound": {
+ "message": "Nie można znaleźć serwera CardDAV."
+ },
+ "status.gContactSync": {
+ "message": "Występuje niezgodność z gContactSync z aktywowaną synchronizacją grupy kontaktów. Dezaktywuj jeden z nich, dopóki błąd nie zostanie rozwiązany."
+ },
+ "status.info.restored": {
+ "message": "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": {
+ "message": "Nie można przeanalizować XML. Sprawdź dziennik zdarzeń, aby uzyskać szczegółowe informacje."
+ },
+ "status.missing-permission": {
+ "message": "Brak uprawnienia: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Nie można połączyć z serwerem."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Błąd niekrytyczny (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Aktualizowanie listy folderów"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Przetwarzanie potwierdzenia lokalnych zmian"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Przetwarzanie zdalnych zmian"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Wysyłanie zmian lokalnych"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Żądanie listy folderów"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Oczekiwanie na potwierdzenie lokalnych zmian"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Oczekiwanie na zdalne zmiany"
+ }
+} \ No newline at end of file
diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json
new file mode 100644
index 0000000..1bd84b5
--- /dev/null
+++ b/_locales/pt_BR/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Propriedades de contato (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Endereço de e-mail:"
+ },
+ "abCard.MiddleName": {
+ "message": "Nome do meio:"
+ },
+ "abCard.Phone": {
+ "message": "Números de telefone"
+ },
+ "abCard.PrefixName": {
+ "message": "Prefixo:"
+ },
+ "abCard.SuffixName": {
+ "message": "Sufixo:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "carro"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "móvel"
+ },
+ "abCard.emailtypes.description": {
+ "message": "O primeiro endereço de email na lista é usado como o endereço de email principal."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "fax"
+ },
+ "abCard.emailtypes.home": {
+ "message": "privadamente"
+ },
+ "abCard.emailtypes.other": {
+ "message": "outro"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "pager"
+ },
+ "abCard.emailtypes.video": {
+ "message": "vídeo"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "telefone"
+ },
+ "abCard.emailtypes.work": {
+ "message": "negócio"
+ },
+ "acl.add": {
+ "message": "adicionar"
+ },
+ "acl.delete": {
+ "message": "excluir"
+ },
+ "acl.modify": {
+ "message": "modificar"
+ },
+ "acl.none": {
+ "message": "nenhum"
+ },
+ "acl.readonly": {
+ "message": "Acesso ao servidor somente leitura (reverter alterações locais)"
+ },
+ "acl.readwrite": {
+ "message": "Permissões de gravação do servidor: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "Endereço servidor CalDAV:"
+ },
+ "add.carddavserver": {
+ "message": "Endereço servidor CardDAV:"
+ },
+ "add.data.description": {
+ "message": "Por favor, forneça um nome amigável para a nova conta TbSync e as credenciais para o seu servidor:"
+ },
+ "add.data.notes": {
+ "message": "Notas:"
+ },
+ "add.data.title": {
+ "message": "Digite as informações da conta"
+ },
+ "add.finish.description": {
+ "message": "?? The following settings have been verified successfully: ??"
+ },
+ "add.finish.details": {
+ "message": "?? Click „Finish“ to create a new TbSync account with these settings. ??"
+ },
+ "add.finish.title": {
+ "message": "?? Confirm account creation ??"
+ },
+ "add.name": {
+ "message": "Nome da conta:"
+ },
+ "add.ok": {
+ "message": "Adicionar conta"
+ },
+ "add.password": {
+ "message": "Senha:"
+ },
+ "add.server": {
+ "message": "URL do servidor:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Configuração manual"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "Os serviços de CalDAV e CardDAV podem ser configurados manualmente."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "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": {
+ "message": "Se você deixar o endereço vazio, o serviço correspondente será desativado para essa conta."
+ },
+ "add.serverprofile.description": {
+ "message": "Por favor, selecione um dos perfis de servidor disponíveis:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Configuração Automática"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "opcional"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "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.gmx.com": {
+ "message": "GMX.com (E.U.A.)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Europa)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "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": {
+ "message": "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.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "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.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Selecione um perfil do servidor"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "A senha solicitada é uma senha específica do aplicativo para TbSync, que você pode criar no seu portal web do Yahoo!. Não é a sua senha padrão do Yahoo!."
+ },
+ "add.spinner.query": {
+ "message": "Enviando pedido RFC6764 para '##replace.1##'"
+ },
+ "add.spinner.validating": {
+ "message": "Verifique a conexão com o servidor"
+ },
+ "add.title": {
+ "message": "Adicionando uma conta CalDAV e CardDAV para o TbSync"
+ },
+ "add.user": {
+ "message": "Usuário:"
+ },
+ "autocomplete.HOME": {
+ "message": "particular"
+ },
+ "autocomplete.PREF": {
+ "message": "preferido"
+ },
+ "autocomplete.WORK": {
+ "message": "empresa"
+ },
+ "config.custom": {
+ "message": "Configurações do servidor CalDAV & CardDAV"
+ },
+ "defaultname.calendar": {
+ "message": "calendário"
+ },
+ "defaultname.contacts": {
+ "message": "contatos"
+ },
+ "extensionDescription": {
+ "message": "Adiciona suporte para sincronizar contas CalDAV e CardDAV com o TbSync."
+ },
+ "extensionName": {
+ "message": "Provedor CalDAV e CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Configurações da conta"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Opções"
+ },
+ "menu.name": {
+ "message": "CalDAV e CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Nome da conta"
+ },
+ "pref.CalDavServer": {
+ "message": "Endereço servidor CalDAV:"
+ },
+ "pref.CardDavServer": {
+ "message": "Endereço servidor CardDAV:"
+ },
+ "pref.UserName": {
+ "message": "Usuário"
+ },
+ "pref.calendaroptions": {
+ "message": "Opções de calendário"
+ },
+ "pref.contactoptions": {
+ "message": "Opções de contato"
+ },
+ "pref.downloadonly": {
+ "message": "Reverter alterações locais (sincronização unidirecional)"
+ },
+ "pref.generaloptions": {
+ "message": "Opções Gerais"
+ },
+ "pref.syncGroups": {
+ "message": "Sincronizar grupos de contatos como listas do Thunderbird"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "NOTA: Devido ao bug do Thunderbird 1522453, o TbSync deverá adicionar endereços de e-mail fictícios a todos os contatos, que fazem parte de grupos, mas não têm um endereço de e-mail associado. Esses endereços de email fictícios não são sincronizados com o servidor."
+ },
+ "pref.useCalendarCache": {
+ "message": "Suporte Offline"
+ },
+ "pref.useCardBook": {
+ "message": "Criar catálogo de endereços no CardBook em vez do catálogo de endereços padrão do Thunderbird"
+ },
+ "status.401": {
+ "message": "Não foi possível autenticar, verifique o nome de usuário e a senha."
+ },
+ "status.403": {
+ "message": "Conexão rejeitada pelo servidor (proibida)."
+ },
+ "status.404": {
+ "message": "HTTP Erro 404 (recurso solicitado não encontrado)."
+ },
+ "status.500": {
+ "message": "Erro de servidor desconhecido (HTTP Erro 500)."
+ },
+ "status.503": {
+ "message": "Serviço indisponível."
+ },
+ "status.caldavservernotfound": {
+ "message": "Não foi possível encontrar um servidor CalDAV."
+ },
+ "status.carddavservernotfound": {
+ "message": "Não foi possível encontrar um servidor CardDAV."
+ },
+ "status.gContactSync": {
+ "message": "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."
+ },
+ "status.info.restored": {
+ "message": "Devido à falta parcial de permissões de gravação, algumas ações foram rejeitadas pelo servidor e revertidas localmente."
+ },
+ "status.malformed-xml": {
+ "message": "Não foi possível analisar XML. Verifique o log de eventos para obter detalhes."
+ },
+ "status.missing-permission": {
+ "message": "Permissão ausente: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Não foi possível conectar-se ao servidor."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "Erro não fatal (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Atualizando lista de pastas"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Processando reconhecimento de alterações locais"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Processando alterações remotas"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Enviando alterações locais"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Solicitando lista de pastas"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Aguardando confirmação de alterações locais"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Esperando por alterações remotas"
+ }
+} \ No newline at end of file
diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json
new file mode 100644
index 0000000..2448cd8
--- /dev/null
+++ b/_locales/ru/messages.json
@@ -0,0 +1,356 @@
+{
+ "abCard.ContactDetails": {
+ "message": "Контакты свойства (CardDAV)"
+ },
+ "abCard.EmailAddresses": {
+ "message": "Адреса электронной почты:"
+ },
+ "abCard.MiddleName": {
+ "message": "Отчество:"
+ },
+ "abCard.Phone": {
+ "message": "Телефонные номера"
+ },
+ "abCard.PrefixName": {
+ "message": "Префикс:"
+ },
+ "abCard.SuffixName": {
+ "message": "Суффикс:"
+ },
+ "abCard.emailtypes.car": {
+ "message": "автомобиль"
+ },
+ "abCard.emailtypes.cell": {
+ "message": "мобильный"
+ },
+ "abCard.emailtypes.description": {
+ "message": "Первый адрес электронной почты в списке используется в качестве основного адреса электронной почты."
+ },
+ "abCard.emailtypes.fax": {
+ "message": "факс"
+ },
+ "abCard.emailtypes.home": {
+ "message": "дома"
+ },
+ "abCard.emailtypes.other": {
+ "message": "другой"
+ },
+ "abCard.emailtypes.pager": {
+ "message": "пейджер"
+ },
+ "abCard.emailtypes.video": {
+ "message": "видео"
+ },
+ "abCard.emailtypes.voice": {
+ "message": "телефон"
+ },
+ "abCard.emailtypes.work": {
+ "message": "бизнес"
+ },
+ "acl.add": {
+ "message": "добавлять"
+ },
+ "acl.delete": {
+ "message": "удалять"
+ },
+ "acl.modify": {
+ "message": "модифицировать"
+ },
+ "acl.none": {
+ "message": "никто"
+ },
+ "acl.readonly": {
+ "message": "только для чтения"
+ },
+ "acl.readwrite": {
+ "message": "Разрешение на запись: ##replace.1##"
+ },
+ "add.caldavserver": {
+ "message": "Адрес сервера CalDAV:"
+ },
+ "add.carddavserver": {
+ "message": "Адрес сервера CardDAV:"
+ },
+ "add.data.description": {
+ "message": "Пожалуйста укажите понятное имя для нового аккаунта TbSync и учетные данные для вашего сервера:"
+ },
+ "add.data.notes": {
+ "message": "Заметки:"
+ },
+ "add.data.title": {
+ "message": "Введите информацию об аккаунте"
+ },
+ "add.finish.description": {
+ "message": "?? The following settings have been verified successfully: ??"
+ },
+ "add.finish.details": {
+ "message": "?? Click „Finish“ to create a new TbSync account with these settings. ??"
+ },
+ "add.finish.title": {
+ "message": "?? Confirm account creation ??"
+ },
+ "add.name": {
+ "message": "Имя аккаунта:"
+ },
+ "add.ok": {
+ "message": "Добавить аккаунт"
+ },
+ "add.password": {
+ "message": "Пароль:"
+ },
+ "add.server": {
+ "message": "Адрес сервера:"
+ },
+ "add.serverprofile.custom": {
+ "message": "Пользовательская конфигурация"
+ },
+ "add.serverprofile.custom.description": {
+ "message": "The CalDAV and CardDAV service endpoints can be configured manually."
+ },
+ "add.serverprofile.custom.details1": {
+ "message": "The required CalDAV & CardDAV service endpoints or the so called principal addresses should be provided by your service provider."
+ },
+ "add.serverprofile.custom.details2": {
+ "message": "Если вы оставите любой адрес пустым, соответствующая служба будет отключена для этого аккаунта."
+ },
+ "add.serverprofile.description": {
+ "message": "Пожалуйста выберите один из доступных профилей сервера:"
+ },
+ "add.serverprofile.discovery": {
+ "message": "Автоматическая настройка"
+ },
+ "add.serverprofile.discovery.description": {
+ "message": "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": {
+ "message": "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": {
+ "message": "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": {
+ "message": "optional"
+ },
+ "add.serverprofile.fruux": {
+ "message": "fruux"
+ },
+ "add.serverprofile.fruux.description": {
+ "message": "fruux - это сервис, который синхронизирует контакты, календари и задачи. Он работает на базе компании поддерживающей протокол sabre/dav и базируется в Германии."
+ },
+ "add.serverprofile.gmx.com": {
+ "message": "GMX.com (США)"
+ },
+ "add.serverprofile.gmx.com.description": {
+ "message": "https://www.gmx.com"
+ },
+ "add.serverprofile.gmx.net": {
+ "message": "GMX.net (Европа)"
+ },
+ "add.serverprofile.gmx.net.description": {
+ "message": "https://www.gmx.net"
+ },
+ "add.serverprofile.google": {
+ "message": "Google"
+ },
+ "add.serverprofile.google.description": {
+ "message": "https://accounts.google.com"
+ },
+ "add.serverprofile.google.details1": {
+ "message": "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."
+ },
+ "add.serverprofile.icloud": {
+ "message": "iCloud"
+ },
+ "add.serverprofile.icloud.description": {
+ "message": "https://www.icloud.com"
+ },
+ "add.serverprofile.icloud.details1": {
+ "message": "Запрашиваемое имя пользователя - ваш Apple-ID. Обратите внимание, что вы не можете использовать свой пароль Apple-ID здесь. Вы ДОЛЖНЫ включить двухфакторную авторизацию для своей учетной записи iCloud и создать отдельный пароль приложения для TbSync."
+ },
+ "add.serverprofile.icloud.details2": {
+ "message": "Это уровень безопасности, применяемый Apple, поэтому сторонние клиенты не могут получить доступ к вашей учетной записи Apple."
+ },
+ "add.serverprofile.mbo": {
+ "message": "mailbox.org"
+ },
+ "add.serverprofile.mbo.description": {
+ "message": "mailbox.org is a secure German e-mail provider for private and business customers, which also offers calendars, contacts and cloud storage."
+ },
+ "add.serverprofile.posteo": {
+ "message": "Posteo"
+ },
+ "add.serverprofile.posteo.description": {
+ "message": "https://www.posteo.de"
+ },
+ "add.serverprofile.title": {
+ "message": "Выберите профиль сервера"
+ },
+ "add.serverprofile.web.de": {
+ "message": "WEB.de"
+ },
+ "add.serverprofile.web.de.description": {
+ "message": "https://www.web.de"
+ },
+ "add.serverprofile.yahoo": {
+ "message": "Yahoo!"
+ },
+ "add.serverprofile.yahoo.description": {
+ "message": "https://www.yahoo.com"
+ },
+ "add.serverprofile.yahoo.details1": {
+ "message": "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.spinner.query": {
+ "message": "Sending RFC6764 request to “##replace.1##”"
+ },
+ "add.spinner.validating": {
+ "message": "Проверьте соединение с сервером"
+ },
+ "add.title": {
+ "message": "ДобавитьCalDAV & CardDAV аккаунт для TbSync"
+ },
+ "add.user": {
+ "message": "Имя пользователя:"
+ },
+ "autocomplete.HOME": {
+ "message": "private"
+ },
+ "autocomplete.PREF": {
+ "message": "preferred"
+ },
+ "autocomplete.WORK": {
+ "message": "business"
+ },
+ "config.custom": {
+ "message": "CalDAV & CardDAV конфигурация сервера"
+ },
+ "defaultname.calendar": {
+ "message": "календарь"
+ },
+ "defaultname.contacts": {
+ "message": "контакты"
+ },
+ "extensionDescription": {
+ "message": "Добавляет в TbSync поддержку базирующегося на http/https протокола синхронизации для учетных записей CalDAV & CardDAV (контакты, задачи и календари)."
+ },
+ "extensionName": {
+ "message": "Provider for CalDAV & CardDAV"
+ },
+ "helplink.malformed-xml": {
+ "message": "https://github.com/jobisoft/DAV-4-TbSync/issues/104"
+ },
+ "manager.tabs.accountsettings": {
+ "message": "Настройки аккаунта"
+ },
+ "manager.tabs.syncsettings": {
+ "message": "Настройки синхронизации"
+ },
+ "menu.name": {
+ "message": "CalDAV & CardDAV"
+ },
+ "pref.AccountName": {
+ "message": "Имя аккаунта"
+ },
+ "pref.CalDavServer": {
+ "message": "Адрес сервера CalDAV:"
+ },
+ "pref.CardDavServer": {
+ "message": "Адрес сервера CardDAV:"
+ },
+ "pref.UserName": {
+ "message": "Имя пользователя"
+ },
+ "pref.calendaroptions": {
+ "message": "Настройки календаря"
+ },
+ "pref.contactoptions": {
+ "message": "Настройки контакта"
+ },
+ "pref.downloadonly": {
+ "message": "Отменить локальные изменения (односторонняя синхронизация)"
+ },
+ "pref.generaloptions": {
+ "message": "Общие настройки"
+ },
+ "pref.syncGroups": {
+ "message": "Синхронизировать группы контактов как списки Thunderbird"
+ },
+ "pref.syncGroupsDescription": {
+ "message": "ПРИМЕЧАНИЕ. В связи с ошибкой TB 1522453, TbSync должен добавить фиктивные адреса электронной почты для всех контактов, которые входят в группы, но не имеют адреса электронной почты. Эти фиктивные адреса электронной почты не синхронизируются с сервером."
+ },
+ "pref.useCalendarCache": {
+ "message": "Офлайн поддержка"
+ },
+ "pref.useCardBook": {
+ "message": "Создавайте адресные книги CardBook вместо стандартных адресных книг Thunderbird"
+ },
+ "status.401": {
+ "message": "Не удалось аутентифицировать, проверить имя пользователя и пароль. (HTTP Ошибка 401)."
+ },
+ "status.403": {
+ "message": "Сервер отклонил соединение (запрещено) (HTTP Ошибка 403)."
+ },
+ "status.404": {
+ "message": "Запрошенный ресурс не найден (HTTP Ошибка 404)."
+ },
+ "status.500": {
+ "message": "Неизвестная ошибка сервера (HTTP Ошибка 500)."
+ },
+ "status.503": {
+ "message": "Сервис недоступен (HTTP Ошибка 503)."
+ },
+ "status.caldavservernotfound": {
+ "message": "Не удалось найти сервер CalDAV."
+ },
+ "status.carddavservernotfound": {
+ "message": "Не удалось найти сервер CardDAV."
+ },
+ "status.gContactSync": {
+ "message": "There is an incompatibility with gContactSync with activated contact group synchronization. Please deactivate one of them as long as the error is not resolved."
+ },
+ "status.info.restored": {
+ "message": "Из-за частично отсутствующих разрешений на запись некоторые действия были отклонены сервером и отменены локально."
+ },
+ "status.malformed-xml": {
+ "message": "Не удалось разобрать XML. Проверьте журнал событий для деталей."
+ },
+ "status.missing-permission": {
+ "message": "Отсутствует разрешение: ##replace.1##"
+ },
+ "status.networkerror": {
+ "message": "Не удалось подключиться к серверу."
+ },
+ "status.rfc6764-lookup-failed": {
+ "message": "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.service-discovery-failed": {
+ "message": "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.softerror": {
+ "message": "пропущенная ошибка (##replace.1##)"
+ },
+ "status.success.managed-by-lightning": {
+ "message": "Lightning"
+ },
+ "syncstate.eval.folders": {
+ "message": "Обновление списка папок"
+ },
+ "syncstate.eval.response.localchanges": {
+ "message": "Обработка подтверждения локальных изменений"
+ },
+ "syncstate.eval.response.remotechanges": {
+ "message": "Обработка удаленных изменений"
+ },
+ "syncstate.prepare.request.localchanges": {
+ "message": "Отправка локальных изменений"
+ },
+ "syncstate.send.getfolders": {
+ "message": "Запрос списка папок"
+ },
+ "syncstate.send.request.localchanges": {
+ "message": "Ожидание подтверждения локальных изменений"
+ },
+ "syncstate.send.request.remotechanges": {
+ "message": "Ожидание удаленных изменений"
+ }
+} \ No newline at end of file
diff --git a/background.js b/background.js
new file mode 100644
index 0000000..d5ca2b2
--- /dev/null
+++ b/background.js
@@ -0,0 +1,13 @@
+function handleUpdateAvailable(details) {
+ console.log("Update available for Dav4TbSync");
+}
+
+async function main() {
+ // just by registering this listener, updates will not install until next restart
+ //messenger.runtime.onUpdateAvailable.addListener(handleUpdateAvailable);
+
+ await messenger.BootstrapLoader.registerChromeUrl([ ["content", "dav4tbsync", "content/"] ]);
+ await messenger.BootstrapLoader.registerBootstrapScript("chrome://dav4tbsync/content/bootstrap.js");
+}
+
+main();
diff --git a/beta-release-channel-update.json b/beta-release-channel-update.json
new file mode 100644
index 0000000..6b172e6
--- /dev/null
+++ b/beta-release-channel-update.json
@@ -0,0 +1,13 @@
+{
+ "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": "78.0" } } }
+ ]
+ }
+ }
+} \ No newline at end of file
diff --git a/content/api/BootstrapLoader/README.md b/content/api/BootstrapLoader/README.md
new file mode 100644
index 0000000..7e8fe2a
--- /dev/null
+++ b/content/api/BootstrapLoader/README.md
@@ -0,0 +1 @@
+Usage description can be found in the [wiki](https://github.com/thundernest/addon-developer-support/wiki/Using-the-BootstrapLoader-API-to-convert-a-Legacy-Bootstrap-WebExtension-into-a-MailExtension-for-Thunderbird-78).
diff --git a/content/api/BootstrapLoader/implementation.js b/content/api/BootstrapLoader/implementation.js
new file mode 100644
index 0000000..f330f9b
--- /dev/null
+++ b/content/api/BootstrapLoader/implementation.js
@@ -0,0 +1,153 @@
+/*
+ * This file is provided by the addon-developer-support repository at
+ * https://github.com/thundernest/addon-developer-support
+ *
+ * Version: 1.3
+ * - flush cache
+ *
+ * Version: 1.2
+ * - add support for resource urls
+ *
+ * Author: John Bieling (john@thunderbird.net)
+ *
+ * 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/.
+ */
+
+// Get various parts of the WebExtension framework that we need.
+var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+
+var BootstrapLoader = class extends ExtensionCommon.ExtensionAPI {
+ getAPI(context) {
+ this.pathToBootstrapScript = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.bootstrappedObj = {};
+
+ // make the extension object and the messenger object available inside
+ // the bootstrapped scope
+ this.bootstrappedObj.extension = context.extension;
+ this.bootstrappedObj.messenger = Array.from(context.extension.views)
+ .find(view => view.viewType === "background")
+ .xulBrowser.contentWindow.wrappedJSObject.browser;
+
+
+ this.BOOTSTRAP_REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ ADDON_ENABLE: 3,
+ ADDON_DISABLE: 4,
+ ADDON_INSTALL: 5,
+ ADDON_UNINSTALL: 6, // not supported
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+ };
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ return {
+ BootstrapLoader: {
+
+ registerChromeUrl(data) {
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerBootstrapScript: async function(aPath) {
+ self.pathToBootstrapScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+
+ // Get the addon object belonging to this extension.
+ let addon = await AddonManager.getAddonByID(context.extension.id);
+ //make the addon globally available in the bootstrapped scope
+ self.bootstrappedObj.addon = addon;
+
+ // add BOOTSTRAP_REASONS to scope
+ for (let reason of Object.keys(self.BOOTSTRAP_REASONS)) {
+ self.bootstrappedObj[reason] = self.BOOTSTRAP_REASONS[reason];
+ }
+
+ // Load registered bootstrap scripts and execute its startup() function.
+ try {
+ if (self.pathToBootstrapScript) Services.scriptloader.loadSubScript(self.pathToBootstrapScript, self.bootstrappedObj, "UTF-8");
+ if (self.bootstrappedObj.startup) self.bootstrappedObj.startup.call(self.bootstrappedObj, self.extension.addonData, self.BOOTSTRAP_REASONS[self.extension.startupReason]);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+ }
+ };
+ }
+
+ onShutdown(isAppShutdown) {
+ // Execute registered shutdown()
+ try {
+ if (this.bootstrappedObj.shutdown) {
+ this.bootstrappedObj.shutdown(
+ this.extension.addonData,
+ isAppShutdown
+ ? this.BOOTSTRAP_REASONS.APP_SHUTDOWN
+ : this.BOOTSTRAP_REASONS.ADDON_DISABLE);
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ console.log("BootstrapLoader for " + this.extension.id + " unloaded!");
+ }
+};
diff --git a/content/api/BootstrapLoader/schema.json b/content/api/BootstrapLoader/schema.json
new file mode 100644
index 0000000..5c544c9
--- /dev/null
+++ b/content/api/BootstrapLoader/schema.json
@@ -0,0 +1,39 @@
+[
+ {
+ "namespace": "BootstrapLoader",
+ "functions": [
+ {
+ "name": "registerChromeUrl",
+ "type": "function",
+ "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
+ "async": true,
+ "parameters": [
+ {
+ "name": "chromeData",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items" : {
+ "type": "string"
+ }
+ },
+ "description": "Array of ChromeData Arrays."
+ }
+ ]
+ },
+ {
+ "name": "registerBootstrapScript",
+ "type": "function",
+ "description": "Register a bootstrap.js style script",
+ "async": true,
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Either the chrome:// path to the script or its relative location from the root of the extension,"
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/content/bootstrap.js b/content/bootstrap.js
new file mode 100644
index 0000000..b8b8936
--- /dev/null
+++ b/content/bootstrap.js
@@ -0,0 +1,77 @@
+/*
+ * 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");
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+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) {
+ Cu.unload("chrome://dav4tbsync/content/includes/GoogleDavCalendar.jsm");
+ Cu.unload("chrome://dav4tbsync/content/includes/GoogleDavSession.jsm");
+ let { GoogleDavCalendar } = ChromeUtils.import(
+ "chrome://dav4tbsync/content/includes/GoogleDavCalendar.jsm"
+ );
+ if (cal.getCalendarManager().wrappedJSObject.hasCalendarProvider("tbSyncCalDav")) {
+ cal.getCalendarManager().wrappedJSObject.unregisterCalendarProvider("tbSyncCalDav", true);
+ }
+ cal.getCalendarManager().wrappedJSObject.registerCalendarProvider("tbSyncCalDav", GoogleDavCalendar);
+ await TbSync.providers.loadProvider(extension, "dav", "chrome://dav4tbsync/content/provider.js");
+ }
+ }
+}
+
+
+function startup(data, reason) {
+ // Possible reasons: APP_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ Services.obs.addObserver(onInitDoneObserver, "tbsync.observer.initialized", false);
+
+ // 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_STARTUP, ADDON_ENABLE, ADDON_INSTALL, ADDON_UPGRADE, or ADDON_DOWNGRADE.
+
+ // When the application is shutting down we normally don't have to clean up.
+ if (reason == APP_SHUTDOWN) {
+ return;
+ }
+
+
+ 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, "startupcache-invalidate");
+ Services.obs.notifyObservers(null, "chrome-flush-caches");
+}
diff --git a/content/includes/GoogleDavCalendar.jsm b/content/includes/GoogleDavCalendar.jsm
new file mode 100644
index 0000000..9819ca9
--- /dev/null
+++ b/content/includes/GoogleDavCalendar.jsm
@@ -0,0 +1,2436 @@
+/* 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 EXPORTED_SYMBOLS = ["GoogleDavCalendar"];
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
+var { CalDavCalendar } = ChromeUtils.import("resource:///modules/CalDavCalendar.jsm");
+
+var {
+ CalDavGenericRequest,
+ CalDavLegacySAXRequest,
+ CalDavItemRequest,
+ CalDavDeleteItemRequest,
+ CalDavPropfindRequest,
+ CalDavHeaderRequest,
+ CalDavPrincipalPropertySearchRequest,
+ CalDavOutboxRequest,
+ CalDavFreeBusyRequest,
+} = ChromeUtils.import("resource:///modules/caldav/CalDavRequest.jsm");
+
+var { CalDavEtagsHandler, CalDavWebDavSyncHandler, CalDavMultigetSyncHandler } = ChromeUtils.import(
+ "resource:///modules/caldav/CalDavRequestHandlers.jsm"
+);
+
+var { GoogleDavSession } = ChromeUtils.import("chrome://dav4tbsync/content/includes/GoogleDavSession.jsm");
+
+var XML_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n';
+var MIME_TEXT_XML = "text/xml; charset=utf-8";
+
+var cIOL = Ci.calIOperationListener;
+
+function GoogleDavCalendar() {
+ this.initProviderBase();
+ this.unmappedProperties = [];
+ this.mUriParams = null;
+ this.mItemInfoCache = {};
+ this.mDisabled = false;
+ this.mCalHomeSet = null;
+ this.mInboxUrl = null;
+ this.mOutboxUrl = null;
+ this.mCalendarUserAddress = null;
+ this.mCheckedServerInfo = null;
+ this.mPrincipalUrl = null;
+ this.mSenderAddress = null;
+ this.mHrefIndex = {};
+ this.mAuthScheme = null;
+ this.mAuthRealm = null;
+ this.mObserver = null;
+ this.mFirstRefreshDone = false;
+ this.mOfflineStorage = null;
+ this.mQueuedQueries = [];
+ this.mCtag = null;
+ this.mProposedCtag = null;
+
+ // By default, support both events and todos.
+ this.mGenerallySupportedItemTypes = ["VEVENT", "VTODO"];
+ this.mSupportedItemTypes = this.mGenerallySupportedItemTypes.slice(0);
+ this.mACLProperties = {};
+
+ this.tbSyncLoaded = false;
+}
+
+// used for etag checking
+var CALDAV_MODIFY_ITEM = "modify";
+var CALDAV_DELETE_ITEM = "delete";
+
+var GoogleDavCalendarClassID = Components.ID('{7eb8f992-3956-4607-95ac-b860ebd51f5a}');
+var GoogleDavCalendarInterfaces = [
+ Ci.calICalendarProvider,
+ Ci.nsIInterfaceRequestor,
+ Ci.calIFreeBusyProvider,
+ Ci.calIItipTransport,
+ Ci.calISchedulingSupport,
+ Ci.calICalendar,
+ Ci.calIChangeLog,
+ Ci.calICalDavCalendar,
+];
+
+GoogleDavCalendar.prototype = {
+ __proto__: cal.provider.BaseClass.prototype,
+ classID: GoogleDavCalendarClassID,
+ QueryInterface: cal.generateQI(GoogleDavCalendarInterfaces),
+ classInfo: cal.generateCI({
+ classID: GoogleDavCalendarClassID,
+ contractID: "@mozilla.org/calendar/calendar;1?type=tbSyncCalDav",
+ classDescription: "Google CalDAV back-end",
+ interfaces: GoogleDavCalendarInterfaces,
+ }),
+
+ // An array of components that are supported by the server. The default is
+ // to support VEVENT and VTODO, if queries for these components return a 4xx
+ // error, then they will be removed from this array.
+ mGenerallySupportedItemTypes: null,
+ mSupportedItemTypes: null,
+ suportedItemTypes: null,
+ get supportedItemTypes() {
+ return this.mSupportedItemTypes;
+ },
+
+ get isCached() {
+ return this != this.superCalendar;
+ },
+
+ mLastRedirectStatus: null,
+
+ ensureTargetCalendar() {
+ if (!this.isCached && !this.mOfflineStorage) {
+ // If this is a cached calendar, the actual cache is taken care of
+ // by the calCachedCalendar facade. In any other case, we use a
+ // memory calendar to cache things.
+ this.mOfflineStorage = Cc["@mozilla.org/calendar/calendar;1?type=memory"].createInstance(
+ Ci.calISyncWriteCalendar
+ );
+
+ this.mOfflineStorage.superCalendar = this;
+ this.mObserver = new calDavObserver(this);
+ this.mOfflineStorage.addObserver(this.mObserver);
+ this.mOfflineStorage.setProperty("relaxedMode", true);
+ }
+ },
+
+ get id() {
+ return this.mID;
+ },
+ set id(val) {
+ let setter = this.__proto__.__proto__.__lookupSetter__("id");
+ val = setter.call(this, val);
+
+ if (this.id) {
+ this.session = new GoogleDavSession(this.id, this.name);
+ }
+ return val;
+ },
+
+ //
+ // calICalendarProvider interface
+ //
+ get prefChromeOverlay() {
+ return null;
+ },
+
+ get displayName() {
+ return cal.l10n.getCalString("caldavName");
+ },
+
+ createCalendar() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ deleteCalendar(_cal, listener) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ // calIChangeLog interface
+ get offlineStorage() {
+ return this.mOfflineStorage;
+ },
+
+ set offlineStorage(storage) {
+ this.mOfflineStorage = storage;
+ this.fetchCachedMetaData();
+ },
+
+ resetLog() {
+ if (this.isCached && this.mOfflineStorage) {
+ this.mOfflineStorage.startBatch();
+ try {
+ for (let itemId in this.mItemInfoCache) {
+ this.mOfflineStorage.deleteMetaData(itemId);
+ delete this.mItemInfoCache[itemId];
+ }
+ } finally {
+ this.mOfflineStorage.endBatch();
+ }
+ }
+ },
+
+ get offlineCachedProperties() {
+ return [
+ "mAuthScheme",
+ "mAuthRealm",
+ "mHasWebdavSyncSupport",
+ "mCtag",
+ "mWebdavSyncToken",
+ "mSupportedItemTypes",
+ "mPrincipalUrl",
+ "mCalHomeSet",
+ "mShouldPollInbox",
+ "mHasAutoScheduling",
+ "mHaveScheduling",
+ "mCalendarUserAddress",
+ "mOutboxUrl",
+ "hasFreeBusy",
+ ];
+ },
+
+ get checkedServerInfo() {
+ if (Services.io.offline) {
+ return true;
+ }
+ return this.mCheckedServerInfo;
+ },
+
+ set checkedServerInfo(val) {
+ return (this.mCheckedServerInfo = val);
+ },
+
+ saveCalendarProperties() {
+ let properties = {};
+ for (let property of this.offlineCachedProperties) {
+ if (this[property] !== undefined) {
+ properties[property] = this[property];
+ }
+ }
+ this.mOfflineStorage.setMetaData("calendar-properties", JSON.stringify(properties));
+ },
+ restoreCalendarProperties(data) {
+ let properties = JSON.parse(data);
+ for (let property of this.offlineCachedProperties) {
+ if (properties[property] !== undefined) {
+ this[property] = properties[property];
+ }
+ }
+ // migration code from bug 1299610
+ if ("hasAutoScheduling" in properties && properties.hasAutoScheduling !== undefined) {
+ this.mHasAutoScheduling = properties.hasAutoScheduling;
+ }
+ },
+
+ // in calIGenericOperationListener aListener
+ replayChangesOn(aChangeLogListener) {
+ if (this.checkedServerInfo) {
+ this.safeRefresh(aChangeLogListener);
+ } else {
+ // If we haven't refreshed yet, then we should check the resource
+ // type first. This will call refresh() again afterwards.
+ this.checkDavResourceType(aChangeLogListener);
+ }
+ },
+ setMetaData(id, path, etag, isInboxItem) {
+ if (this.mOfflineStorage.setMetaData) {
+ if (id) {
+ let dataString = [etag, path, isInboxItem ? "true" : "false"].join("\u001A");
+ this.mOfflineStorage.setMetaData(id, dataString);
+ } else {
+ cal.LOG("CalDAV: cannot store meta data without an id");
+ }
+ } else {
+ cal.ERROR("CalDAV: calendar storage does not support meta data");
+ }
+ },
+
+ /**
+ * Ensure that cached items have associated meta data, otherwise server side
+ * changes may not be reflected
+ */
+ ensureMetaData() {
+ let self = this;
+ let refreshNeeded = false;
+ let getMetaListener = {
+ QueryInterface: ChromeUtils.generateQI([Ci.calIOperationListener]),
+ onGetResult(aCalendar, aStatus, aItemType, aDetail, aItems) {
+ for (let item of aItems) {
+ if (!(item.id in self.mItemInfoCache)) {
+ let path = self.getItemLocationPath(item);
+ cal.LOG("Adding meta-data for cached item " + item.id);
+ self.mItemInfoCache[item.id] = {
+ etag: null,
+ isNew: false,
+ locationPath: path,
+ isInboxItem: false,
+ };
+ self.mHrefIndex[self.mLocationPath + path] = item.id;
+ refreshNeeded = true;
+ }
+ }
+ },
+ onOperationComplete(aCalendar, aStatus, aOpType, aId, aDetail) {
+ if (refreshNeeded) {
+ // resetting the cached ctag forces an item refresh when
+ // safeRefresh is called later
+ self.mCtag = null;
+ self.mProposedCtag = null;
+ }
+ },
+ };
+ this.mOfflineStorage.getItems(
+ Ci.calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 0,
+ null,
+ null,
+ getMetaListener
+ );
+ },
+
+ fetchCachedMetaData() {
+ cal.LOG("CalDAV: Retrieving server info from cache for " + this.name);
+ let cacheIds = this.mOfflineStorage.getAllMetaDataIds();
+ let cacheValues = this.mOfflineStorage.getAllMetaDataValues();
+
+ for (let count = 0; count < cacheIds.length; count++) {
+ let itemId = cacheIds[count];
+ let itemData = cacheValues[count];
+ if (itemId == "ctag") {
+ this.mCtag = itemData;
+ this.mProposedCtag = null;
+ this.mOfflineStorage.deleteMetaData("ctag");
+ } else if (itemId == "webdav-sync-token") {
+ this.mWebdavSyncToken = itemData;
+ this.mOfflineStorage.deleteMetaData("sync-token");
+ } else if (itemId == "calendar-properties") {
+ this.restoreCalendarProperties(itemData);
+ this.setProperty("currentStatus", Cr.NS_OK);
+ if (this.mHaveScheduling || this.hasAutoScheduling || this.hasFreeBusy) {
+ cal.getFreeBusyService().addProvider(this);
+ }
+ } else {
+ let itemDataArray = itemData.split("\u001A");
+ let etag = itemDataArray[0];
+ let resourcePath = itemDataArray[1];
+ let isInboxItem = itemDataArray[2];
+ if (itemDataArray.length == 3) {
+ this.mHrefIndex[resourcePath] = itemId;
+ let locationPath = resourcePath.substr(this.mLocationPath.length);
+ let item = {
+ etag,
+ isNew: false,
+ locationPath,
+ isInboxItem: isInboxItem == "true",
+ };
+ this.mItemInfoCache[itemId] = item;
+ }
+ }
+ }
+
+ this.ensureMetaData();
+ },
+
+ //
+ // calICalendar interface
+ //
+
+ // readonly attribute AUTF8String type;
+ get type() {
+ return "tbSyncCalDav";
+ },
+
+ mDisabled: true,
+
+ mCalendarUserAddress: null,
+ get calendarUserAddress() {
+ return this.mCalendarUserAddress;
+ },
+
+ mPrincipalUrl: null,
+ get principalUrl() {
+ return this.mPrincipalUrl;
+ },
+
+ get canRefresh() {
+ // A cached calendar doesn't need to be refreshed.
+ return !this.isCached;
+ },
+
+ // mUriParams stores trailing ?parameters from the
+ // supplied calendar URI. Needed for (at least) Cosmo
+ // tickets
+ mUriParams: null,
+
+ get uri() {
+ return this.mUri;
+ },
+
+ set uri(aUri) {
+ this.mUri = aUri;
+
+ return aUri;
+ },
+
+ get calendarUri() {
+ let calSpec = this.mUri.spec;
+ let parts = calSpec.split("?");
+ if (parts.length > 1) {
+ calSpec = parts.shift();
+ this.mUriParams = "?" + parts.join("?");
+ }
+ if (!calSpec.endsWith("/")) {
+ calSpec += "/";
+ }
+ return Services.io.newURI(calSpec);
+ },
+
+ setCalHomeSet(removeLastPathSegment) {
+ if (removeLastPathSegment) {
+ let split1 = this.mUri.spec.split("?");
+ let baseUrl = split1[0];
+ if (baseUrl.charAt(baseUrl.length - 1) == "/") {
+ baseUrl = baseUrl.substring(0, baseUrl.length - 2);
+ }
+ let split2 = baseUrl.split("/");
+ split2.pop();
+ this.mCalHomeSet = Services.io.newURI(split2.join("/") + "/");
+ } else {
+ this.mCalHomeSet = this.calendarUri;
+ }
+ },
+
+ mOutboxUrl: null,
+ get outboxUrl() {
+ return this.mOutboxUrl;
+ },
+
+ mInboxUrl: null,
+ get inboxUrl() {
+ return this.mInboxUrl;
+ },
+
+ mHaveScheduling: false,
+ mShouldPollInbox: true,
+ get hasScheduling() {
+ // Whether to use inbox/outbox scheduling
+ return this.mHaveScheduling;
+ },
+ set hasScheduling(value) {
+ return (this.mHaveScheduling =
+ Services.prefs.getBoolPref("calendar.caldav.sched.enabled", false) && value);
+ },
+ mHasAutoScheduling: false, // Whether server automatically takes care of scheduling
+ get hasAutoScheduling() {
+ return this.mHasAutoScheduling;
+ },
+
+ hasFreebusy: false,
+
+ mAuthScheme: null,
+
+ mAuthRealm: null,
+
+ mFirstRefreshDone: false,
+
+ mQueuedQueries: null,
+
+ mCtag: null,
+ mProposedCtag: null,
+
+ mOfflineStorage: null,
+ // Contains the last valid synctoken returned
+ // from the server with Webdav Sync enabled servers
+ mWebdavSyncToken: null,
+ // Indicates that the server supports Webdav Sync
+ // see: http://tools.ietf.org/html/draft-daboo-webdav-sync
+ mHasWebdavSyncSupport: false,
+
+ get authRealm() {
+ return this.mAuthRealm;
+ },
+
+ /**
+ * Builds a correctly encoded nsIURI based on the baseUri and the insert
+ * string. The returned uri is basically the baseURI + aInsertString
+ *
+ * @param {string} aInsertString - String to append to the base uri, for example,
+ * when creating an event this would be the
+ * event file name (event.ics). If null, an empty
+ * string is used.
+ * @param {nsIURI} aBaseUri - Base uri, if null, this.calendarUri will be used.
+ */
+ makeUri(aInsertString, aBaseUri) {
+ let baseUri = aBaseUri || this.calendarUri;
+ // Build a string containing the full path, decoded, so it looks like
+ // this:
+ // /some path/insert string.ics
+ let decodedPath = this.ensureDecodedPath(baseUri.pathQueryRef) + (aInsertString || "");
+
+ // Build the nsIURI by specifying a string with a fully encoded path
+ // the end result will be something like this:
+ // http://caldav.example.com:8080/some%20path/insert%20string.ics
+ return Services.io.newURI(
+ baseUri.prePath + this.ensureEncodedPath(decodedPath) + (this.mUriParams || "")
+ );
+ },
+
+ get mLocationPath() {
+ return this.ensureDecodedPath(this.calendarUri.pathQueryRef);
+ },
+
+ getItemLocationPath(aItem) {
+ if (aItem.id && aItem.id in this.mItemInfoCache && this.mItemInfoCache[aItem.id].locationPath) {
+ // modifying items use the cached location path
+ return this.mItemInfoCache[aItem.id].locationPath;
+ }
+ // New items just use id.ics
+ return aItem.id + ".ics";
+ },
+
+ getProperty(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;
+ }
+ return this.__proto__.__proto__.getProperty.apply(this, arguments);
+ },
+
+ promptOverwrite(aMethod, aItem, aListener, aOldItem) {
+ let overwrite = cal.provider.promptOverwrite(aMethod, aItem, aListener, aOldItem);
+ if (overwrite) {
+ if (aMethod == CALDAV_MODIFY_ITEM) {
+ this.doModifyItem(aItem, aOldItem, aListener, true);
+ } else {
+ this.doDeleteItem(aItem, aListener, true, false, null);
+ }
+ } else {
+ this.getUpdatedItem(aItem, aListener);
+ }
+ },
+
+ mItemInfoCache: null,
+
+ mHrefIndex: null,
+
+ /**
+ * addItem()
+ * we actually use doAdoptItem()
+ *
+ * @param aItem item to add
+ * @param aListener listener for method completion
+ */
+ addItem(aItem, aListener) {
+ return this.doAdoptItem(aItem.clone(), aListener);
+ },
+
+ /**
+ * adoptItem()
+ * we actually use doAdoptItem()
+ *
+ * @param aItem item to check
+ * @param aListener listener for method completion
+ */
+ adoptItem(aItem, aListener) {
+ return this.doAdoptItem(aItem, aListener);
+ },
+
+ /**
+ * Performs the actual addition of the item to CalDAV store
+ *
+ * @param aItem item to add
+ * @param aListener listener for method completion
+ * @param aIgnoreEtag flag to indicate ignoring of Etag
+ */
+ doAdoptItem(aItem, aListener, aIgnoreEtag) {
+ let notifyListener = (status, detail, pure = false) => {
+ let method = pure ? "notifyPureOperationComplete" : "notifyOperationComplete";
+ this[method](aListener, status, cIOL.ADD, aItem.id, detail);
+ };
+ if (aItem.id == null && aItem.isMutable) {
+ aItem.id = cal.getUUID();
+ }
+
+ if (aItem.id == null) {
+ notifyListener(Cr.NS_ERROR_FAILURE, "Can't set ID on non-mutable item to addItem");
+ return;
+ }
+
+ if (!cal.item.isItemSupported(aItem, this)) {
+ notifyListener(Cr.NS_ERROR_FAILURE, "Server does not support item type");
+ return;
+ }
+
+ let parentItem = aItem.parentItem;
+ parentItem.calendar = this.superCalendar;
+
+ let locationPath = this.getItemLocationPath(parentItem);
+ let itemUri = this.makeUri(locationPath);
+ cal.LOG("CalDAV: itemUri.spec = " + itemUri.spec);
+
+ let serializedItem = this.getSerializedItem(aItem);
+
+ let sendEtag = aIgnoreEtag ? null : "*";
+ let request = new CalDavItemRequest(this.session, this, itemUri, aItem, sendEtag);
+
+ request.commit().then(
+ response => {
+ let status = Cr.NS_OK;
+ let detail = parentItem;
+
+ // Translate the HTTP status code to a status and message for the listener
+ if (response.ok) {
+ cal.LOG(`CalDAV: Item added to ${this.name} successfully`);
+
+ let uriComponentParts = this.makeUri()
+ .pathQueryRef.replace(/\/{2,}/g, "/")
+ .split("/").length;
+ let targetParts = response.uri.pathQueryRef.split("/");
+ targetParts.splice(0, uriComponentParts - 1);
+
+ this.mItemInfoCache[parentItem.id] = { locationPath: targetParts.join("/") };
+ // TODO: onOpComplete adds the item to the cache, probably after getUpdatedItem!
+
+ // Some CalDAV servers will modify items on PUT (add X-props,
+ // for instance) so we'd best re-fetch in order to know
+ // the current state of the item
+ // Observers will be notified in getUpdatedItem()
+ this.getUpdatedItem(parentItem, aListener);
+ return;
+ } else if (response.serverError) {
+ status = Cr.NS_ERROR_NOT_AVAILABLE;
+ detail = "Server Replied with " + response.status;
+ } else if (response.status) {
+ // There is a response status, but we haven't handled it yet. Any
+ // error occurring here should consider being handled!
+ cal.ERROR(
+ "CalDAV: Unexpected status adding item to " +
+ this.name +
+ ": " +
+ response.status +
+ "\n" +
+ serializedItem
+ );
+
+ status = Cr.NS_ERROR_FAILURE;
+ detail = "Server Replied with " + response.status;
+ }
+
+ // Still need to visually notify for uncached calendars.
+ if (!this.isCached && !Components.isSuccessCode(status)) {
+ this.reportDavError(Ci.calIErrors.DAV_PUT_ERROR, status, detail);
+ }
+
+ // Finally, notify listener.
+ notifyListener(status, detail, true);
+ },
+ e => {
+ notifyListener(Cr.NS_ERROR_NOT_AVAILABLE, "Error preparing http channel: " + e);
+ }
+ );
+ },
+
+ /**
+ * modifyItem(); required by calICalendar.idl
+ * we actually use doModifyItem()
+ *
+ * @param aItem item to check
+ * @param aListener listener for method completion
+ */
+ modifyItem(aNewItem, aOldItem, aListener) {
+ return this.doModifyItem(aNewItem, aOldItem, aListener, false);
+ },
+
+ /**
+ * Modifies existing item in CalDAV store.
+ *
+ * @param aItem item to check
+ * @param aOldItem previous version of item to be modified
+ * @param aListener listener from original request
+ * @param aIgnoreEtag ignore item etag
+ */
+ doModifyItem(aNewItem, aOldItem, aListener, aIgnoreEtag) {
+ let notifyListener = (status, detail, pure = false) => {
+ let method = pure ? "notifyPureOperationComplete" : "notifyOperationComplete";
+ this[method](aListener, status, cIOL.MODIFY, aNewItem.id, detail);
+ };
+ if (aNewItem.id == null) {
+ notifyListener(Cr.NS_ERROR_FAILURE, "ID for modifyItem doesn't exist or is null");
+ return;
+ }
+
+ let wasInboxItem = this.mItemInfoCache[aNewItem.id].isInboxItem;
+
+ let newItem_ = aNewItem;
+ aNewItem = aNewItem.parentItem.clone();
+ if (newItem_.parentItem != newItem_) {
+ aNewItem.recurrenceInfo.modifyException(newItem_, false);
+ }
+ aNewItem.generation += 1;
+
+ let eventUri = this.makeUri(this.mItemInfoCache[aNewItem.id].locationPath);
+ let modifiedItemICS = this.getSerializedItem(aNewItem);
+
+ let sendEtag = aIgnoreEtag ? null : this.mItemInfoCache[aNewItem.id].etag;
+ let request = new CalDavItemRequest(this.session, this, eventUri, aNewItem, sendEtag);
+
+ request.commit().then(
+ response => {
+ let status = Cr.NS_OK;
+ let detail = aNewItem;
+
+ let shouldNotify = true;
+ if (response.ok) {
+ cal.LOG("CalDAV: Item modified successfully on " + this.name);
+
+ // Some CalDAV servers will modify items on PUT (add X-props, for instance) so we'd
+ // best re-fetch in order to know the current state of the item Observers will be
+ // notified in getUpdatedItem()
+ this.getUpdatedItem(aNewItem, aListener);
+
+ // SOGo has calendarUri == inboxUri so we need to be careful about deletions
+ if (wasInboxItem && this.mShouldPollInbox) {
+ this.doDeleteItem(aNewItem, null, true, true, null);
+ }
+ shouldNotify = false;
+ } else if (response.conflict) {
+ // promptOverwrite will ask the user and then re-request
+ this.promptOverwrite(CALDAV_MODIFY_ITEM, aNewItem, aListener, aOldItem);
+ shouldNotify = false;
+ } else if (response.serverError) {
+ status = Cr.NS_ERROR_NOT_AVAILABLE;
+ detail = "Server Replied with " + response.status;
+ } else if (response.status) {
+ // There is a response status, but we haven't handled it yet. Any error occurring
+ // here should consider being handled!
+ cal.ERROR(
+ "CalDAV: Unexpected status modifying item to " +
+ this.name +
+ ": " +
+ response.status +
+ "\n" +
+ modifiedItemICS
+ );
+
+ status = Cr.NS_ERROR_FAILURE;
+ detail = "Server Replied with " + response.status;
+ }
+
+ if (shouldNotify) {
+ // Still need to visually notify for uncached calendars.
+ if (!this.isCached && !Components.isSuccessCode(status)) {
+ this.reportDavError(Ci.calIErrors.DAV_PUT_ERROR, status, detail);
+ }
+
+ notifyListener(status, detail, true);
+ }
+ },
+ () => {
+ notifyListener(Cr.NS_ERROR_NOT_AVAILABLE, "Error preparing http channel");
+ }
+ );
+ },
+
+ /**
+ * deleteItem(); required by calICalendar.idl
+ * the actual deletion is done in doDeleteItem()
+ *
+ * @param aItem item to delete
+ * @param aListener listener for method completion
+ */
+ deleteItem(aItem, aListener) {
+ return this.doDeleteItem(aItem, aListener, false, null, null);
+ },
+
+ /**
+ * Deletes item from CalDAV store.
+ *
+ * @param aItem item to delete
+ * @param aListener listener for method completion
+ * @param aIgnoreEtag ignore item etag
+ * @param aFromInbox delete from inbox rather than calendar
+ * @param aUri uri of item to delete
+ * */
+ doDeleteItem(aItem, aListener, aIgnoreEtag, aFromInbox, aUri) {
+ let notifyListener = (status, detail, pure = false, report = true) => {
+ // Still need to visually notify for uncached calendars.
+ if (!this.isCached && !Components.isSuccessCode(status)) {
+ this.reportDavError(Ci.calIErrors.DAV_REMOVE_ERROR, status, detail);
+ }
+
+ let method = pure ? "notifyPureOperationComplete" : "notifyOperationComplete";
+ this[method](aListener, status, cIOL.DELETE, aItem.id, detail);
+ };
+
+ if (aItem.id == null) {
+ notifyListener(Cr.NS_ERROR_FAILURE, "ID doesn't exist for deleteItem");
+ return;
+ }
+
+ let eventUri;
+ if (aUri) {
+ eventUri = aUri;
+ } else if (aFromInbox || this.mItemInfoCache[aItem.id].isInboxItem) {
+ eventUri = this.makeUri(this.mItemInfoCache[aItem.id].locationPath, this.mInboxUrl);
+ } else {
+ eventUri = this.makeUri(this.mItemInfoCache[aItem.id].locationPath);
+ }
+
+ if (eventUri.pathQueryRef == this.calendarUri.pathQueryRef) {
+ notifyListener(
+ Cr.NS_ERROR_FAILURE,
+ "eventUri and calendarUri paths are the same, will not go on to delete entire calendar"
+ );
+ return;
+ }
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: Deleting " + eventUri.spec);
+ }
+
+ let sendEtag = aIgnoreEtag ? null : this.mItemInfoCache[aItem.id].etag;
+ let request = new CalDavDeleteItemRequest(this.session, this, eventUri, sendEtag);
+
+ request.commit().then(
+ response => {
+ if (response.ok) {
+ if (!aFromInbox) {
+ let decodedPath = this.ensureDecodedPath(eventUri.pathQueryRef);
+ delete this.mHrefIndex[decodedPath];
+ delete this.mItemInfoCache[aItem.id];
+ cal.LOG("CalDAV: Item deleted successfully from calendar " + this.name);
+
+ if (this.isCached) {
+ notifyListener(Cr.NS_OK, aItem);
+ } else {
+ // If the calendar is not cached, we need to remove
+ // the item from our memory calendar now. The
+ // listeners will be notified there.
+ this.mOfflineStorage.deleteItem(aItem, aListener);
+ }
+ }
+ } else if (response.conflict) {
+ // item has either been modified or deleted by someone else check to see which
+ cal.LOG("CalDAV: Item has been modified on server, checking if it has been deleted");
+ let headrequest = new CalDavGenericRequest(this.session, this, "HEAD", eventUri);
+
+ return headrequest.commit().then(headresponse => {
+ if (headresponse.notFound) {
+ // Nothing to do. Someone else has already deleted it
+ notifyListener(Cr.NS_OK, aItem, true);
+ } else if (headresponse.serverError) {
+ notifyListener(
+ Cr.NS_ERROR_NOT_AVAILABLE,
+ "Server Replied with " + headresponse.status,
+ true
+ );
+ } else if (headresponse.status) {
+ // The item still exists. We need to ask the user if he
+ // really wants to delete the item. Remember, we only
+ // made this request since the actual delete gave 409/412
+ this.promptOverwrite(CALDAV_DELETE_ITEM, aItem, aListener, null);
+ }
+ });
+ } else if (response.serverError) {
+ notifyListener(Cr.NS_ERROR_NOT_AVAILABLE, "Server Replied with " + response.status);
+ } else if (response.status) {
+ cal.ERROR(
+ "CalDAV: Unexpected status deleting item from " +
+ this.name +
+ ": " +
+ response.status +
+ "\n" +
+ "uri: " +
+ eventUri.spec
+ );
+
+ notifyListener(Cr.NS_ERROR_FAILURE, "Server Replied with " + response.status);
+ }
+ return null;
+ },
+ () => {
+ notifyListener(Cr.NS_ERROR_NOT_AVAILABLE, "Error preparing http channel");
+ }
+ );
+ },
+
+ /**
+ * Add an item to the target calendar
+ *
+ * @param path Item path MUST NOT BE ENCODED
+ * @param calData iCalendar string representation of the item
+ * @param aUri Base URI of the request
+ * @param aListener Listener
+ */
+ addTargetCalendarItem(path, calData, aUri, etag, aListener) {
+ let parser = Cc["@mozilla.org/calendar/ics-parser;1"].createInstance(Ci.calIIcsParser);
+ // aUri.pathQueryRef may contain double slashes whereas path does not
+ // this confuses our counting, so remove multiple successive slashes
+ let strippedUriPath = aUri.pathQueryRef.replace(/\/{2,}/g, "/");
+ let uriPathComponentLength = strippedUriPath.split("/").length;
+ try {
+ parser.parseString(calData);
+ } catch (e) {
+ // Warn and continue.
+ // TODO As soon as we have activity manager integration,
+ // this should be replace with logic to notify that a
+ // certain event failed.
+ cal.WARN("Failed to parse item: " + calData + "\n\nException:" + e);
+ return;
+ }
+ // with CalDAV there really should only be one item here
+ let items = parser.getItems();
+ let propertiesList = parser.getProperties();
+ let method;
+ for (let prop of propertiesList) {
+ if (prop.propertyName == "METHOD") {
+ method = prop.value;
+ break;
+ }
+ }
+ let isReply = method == "REPLY";
+ let item = items[0];
+
+ if (!item) {
+ cal.WARN("Failed to parse item: " + calData);
+ return;
+ }
+
+ item.calendar = this.superCalendar;
+ if (isReply && this.isInbox(aUri.spec)) {
+ if (this.hasScheduling) {
+ this.processItipReply(item, path);
+ }
+ cal.WARN("REPLY method but calendar does not support scheduling");
+ return;
+ }
+
+ // Strip of the same number of components as the request
+ // uri's path has. This way we make sure to handle servers
+ // that pass paths like /dav/user/Calendar while
+ // the request uri is like /dav/user@example.org/Calendar.
+ let resPathComponents = path.split("/");
+ resPathComponents.splice(0, uriPathComponentLength - 1);
+ let locationPath = resPathComponents.join("/");
+ let isInboxItem = this.isInbox(aUri.spec);
+
+ if (this.mHrefIndex[path] && !this.mItemInfoCache[item.id]) {
+ // If we get here it means a meeting has kept the same filename
+ // but changed its uid, which can happen server side.
+ // Delete the meeting before re-adding it
+ this.deleteTargetCalendarItem(path);
+ }
+
+ if (this.mItemInfoCache[item.id]) {
+ this.mItemInfoCache[item.id].isNew = false;
+ } else {
+ this.mItemInfoCache[item.id] = { isNew: true };
+ }
+ this.mItemInfoCache[item.id].locationPath = locationPath;
+ this.mItemInfoCache[item.id].isInboxItem = isInboxItem;
+
+ this.mHrefIndex[path] = item.id;
+ this.mItemInfoCache[item.id].etag = etag;
+
+ let needsAddModify = false;
+ if (this.isCached) {
+ this.setMetaData(item.id, path, etag, isInboxItem);
+
+ // If we have a listener, then the caller will take care of adding the item
+ // Otherwise, we have to do it ourself
+ // XXX This is quite fragile, but saves us a double modify/add
+
+ if (aListener) {
+ // In the cached case, notifying operation complete will add the item to the cache
+ if (this.mItemInfoCache[item.id].isNew) {
+ this.notifyOperationComplete(aListener, Cr.NS_OK, cIOL.ADD, item.id, item);
+ } else {
+ this.notifyOperationComplete(aListener, Cr.NS_OK, cIOL.MODIFY, item.id, item);
+ }
+ } else {
+ // No listener, we'll have to add it ourselves
+ needsAddModify = true;
+ }
+ } else {
+ // In the uncached case, we need to do so ourselves
+ needsAddModify = true;
+ }
+
+ // Now take care of the add/modify if needed.
+ if (needsAddModify) {
+ if (this.mItemInfoCache[item.id].isNew) {
+ this.mOfflineStorage.adoptItem(item, aListener);
+ } else {
+ this.mOfflineStorage.modifyItem(item, null, aListener);
+ }
+ }
+ },
+
+ /**
+ * Deletes an item from the target calendar
+ *
+ * @param path Path of the item to delete, must not be encoded
+ */
+ async deleteTargetCalendarItem(path) {
+ let pcal = cal.async.promisifyCalendar(this.mOfflineStorage);
+
+ let foundItem = (await pcal.getItem(this.mHrefIndex[path]))[0];
+ let wasInboxItem = this.mItemInfoCache[foundItem.id].isInboxItem;
+ if ((wasInboxItem && this.isInbox(path)) || (wasInboxItem === false && !this.isInbox(path))) {
+ cal.LOG("CalDAV: deleting item: " + path + ", uid: " + foundItem.id);
+ delete this.mHrefIndex[path];
+ delete this.mItemInfoCache[foundItem.id];
+ if (this.isCached) {
+ this.mOfflineStorage.deleteMetaData(foundItem.id);
+ }
+ await pcal.deleteItem(foundItem);
+ }
+ },
+
+ /**
+ * Perform tasks required after updating items in the calendar such as
+ * notifying the observers and listeners
+ *
+ * @param aChangeLogListener Change log listener
+ * @param calendarURI URI of the calendar whose items just got
+ * changed
+ */
+ finalizeUpdatedItems(aChangeLogListener, calendarURI) {
+ cal.LOG(
+ "aChangeLogListener=" +
+ aChangeLogListener +
+ "\n" +
+ "calendarURI=" +
+ (calendarURI ? calendarURI.spec : "undefined") +
+ " \n" +
+ "iscached=" +
+ this.isCached +
+ "\n" +
+ "this.mQueuedQueries.length=" +
+ this.mQueuedQueries.length
+ );
+ if (this.isCached) {
+ if (aChangeLogListener) {
+ aChangeLogListener.onResult({ status: Cr.NS_OK }, Cr.NS_OK);
+ }
+ } else {
+ this.mObservers.notify("onLoad", [this]);
+ }
+
+ if (this.mProposedCtag) {
+ this.mCtag = this.mProposedCtag;
+ this.mProposedCtag = null;
+ }
+
+ this.mFirstRefreshDone = true;
+ while (this.mQueuedQueries.length) {
+ let query = this.mQueuedQueries.pop();
+ this.mOfflineStorage.getItems(...query);
+ }
+ if (this.hasScheduling && !this.isInbox(calendarURI.spec)) {
+ this.pollInbox();
+ }
+ },
+
+ /**
+ * Notifies the caller that a get request has failed.
+ *
+ * @param errorMsg Error message
+ * @param aListener (optional) Listener of the request
+ * @param aChangeLogListener (optional)Listener for cached calendars
+ */
+ notifyGetFailed(errorMsg, aListener, aChangeLogListener) {
+ cal.WARN("CalDAV: Get failed: " + errorMsg);
+
+ // Notify changelog listener
+ if (this.isCached && aChangeLogListener) {
+ aChangeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE);
+ }
+
+ // Notify operation listener
+ this.notifyOperationComplete(aListener, Cr.NS_ERROR_FAILURE, cIOL.GET, null, errorMsg);
+ // If an error occurs here, we also need to unqueue the
+ // requests previously queued.
+ while (this.mQueuedQueries.length) {
+ let [, , , , listener] = this.mQueuedQueries.pop();
+ try {
+ listener.onOperationComplete(
+ this.superCalendar,
+ Cr.NS_ERROR_FAILURE,
+ cIOL.GET,
+ null,
+ errorMsg
+ );
+ } catch (e) {
+ cal.ERROR(e);
+ }
+ }
+ },
+
+ /**
+ * Retrieves a specific item from the CalDAV store.
+ * Use when an outdated copy of the item is in hand.
+ *
+ * @param aItem item to fetch
+ * @param aListener listener for method completion
+ */
+ getUpdatedItem(aItem, aListener, aChangeLogListener) {
+ if (aItem == null) {
+ this.notifyOperationComplete(
+ aListener,
+ Cr.NS_ERROR_FAILURE,
+ cIOL.GET,
+ null,
+ "passed in null item"
+ );
+ return;
+ }
+
+ let locationPath = this.getItemLocationPath(aItem);
+ let itemUri = this.makeUri(locationPath);
+
+ let multiget = new CalDavMultigetSyncHandler(
+ [this.ensureDecodedPath(itemUri.pathQueryRef)],
+ this,
+ this.makeUri(),
+ null,
+ false,
+ aListener,
+ aChangeLogListener
+ );
+ multiget.doMultiGet();
+ },
+
+ // void getItem( in string id, in calIOperationListener aListener );
+ getItem(aId, aListener) {
+ this.mOfflineStorage.getItem(aId, aListener);
+ },
+
+ // void getItems( in unsigned long aItemFilter, in unsigned long aCount,
+ // in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
+ // in calIOperationListener aListener );
+ getItems(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener) {
+ if (this.isCached) {
+ if (this.mOfflineStorage) {
+ this.mOfflineStorage.getItems(...arguments);
+ } else {
+ this.notifyOperationComplete(aListener, Cr.NS_OK, cIOL.GET, null, null);
+ }
+ } else if (
+ this.checkedServerInfo ||
+ this.getProperty("currentStatus") == Ci.calIErrors.READ_FAILED
+ ) {
+ this.mOfflineStorage.getItems(...arguments);
+ } else {
+ this.mQueuedQueries.push(Array.from(arguments));
+ }
+ },
+
+ fillACLProperties() {
+ let orgId = this.calendarUserAddress;
+ if (orgId) {
+ this.mACLProperties.organizerId = orgId;
+ }
+
+ if (this.mACLEntry && this.mACLEntry.hasAccessControl) {
+ let ownerIdentities = this.mACLEntry.getOwnerIdentities();
+ if (ownerIdentities.length > 0) {
+ let identity = ownerIdentities[0];
+ this.mACLProperties.organizerId = identity.email;
+ this.mACLProperties.organizerCN = identity.fullName;
+ this.mACLProperties["imip.identity"] = identity;
+ }
+ }
+ },
+
+ safeRefresh(aChangeLogListener) {
+ let notifyListener = status => {
+ if (this.isCached && aChangeLogListener) {
+ aChangeLogListener.onResult({ status }, status);
+ }
+ };
+
+ if (!this.mACLEntry) {
+ let self = this;
+ let opListener = {
+ QueryInterface: ChromeUtils.generateQI([Ci.calIOperationListener]),
+ onGetResult(calendar, status, itemType, detail, items) {
+ cal.ASSERT(false, "unexpected!");
+ },
+ onOperationComplete(opCalendar, opStatus, opType, opId, opDetail) {
+ self.mACLEntry = opDetail;
+ self.fillACLProperties();
+ self.safeRefresh(aChangeLogListener);
+ },
+ };
+
+ this.aclManager.getCalendarEntry(this, opListener);
+ return;
+ }
+
+ this.ensureTargetCalendar();
+
+ if (this.mAuthScheme == "Digest") {
+ // the auth could have timed out and be in need of renegotiation we can't risk several
+ // calendars doing this simultaneously so we'll force the renegotiation in a sync query,
+ // using OPTIONS to keep it quick
+ let headchannel = cal.provider.prepHttpChannel(this.makeUri(), null, null, this);
+ headchannel.requestMethod = "OPTIONS";
+ headchannel.open();
+ headchannel.QueryInterface(Ci.nsIHttpChannel);
+ try {
+ if (headchannel.responseStatus != 200) {
+ throw new Error("OPTIONS returned unexpected status code: " + headchannel.responseStatus);
+ }
+ } catch (e) {
+ cal.WARN("CalDAV: Exception: " + e);
+ notifyListener(Cr.NS_ERROR_FAILURE);
+ }
+ }
+
+ // Call getUpdatedItems right away if its the first refresh *OR* if webdav Sync is enabled
+ // (It is redundant to send a request to get the collection tag (getctag) on a calendar if
+ // it supports webdav sync, the sync request will only return data if something changed).
+ if (!this.mCtag || !this.mFirstRefreshDone || this.mHasWebdavSyncSupport) {
+ this.getUpdatedItems(this.calendarUri, aChangeLogListener);
+ return;
+ }
+ let request = new CalDavPropfindRequest(this.session, this, this.makeUri(), ["CS:getctag"]);
+
+ request.commit().then(response => {
+ cal.LOG(`CalDAV: Status ${response.status} checking ctag for calendar ${this.name}`);
+
+ if (response.status == -1) {
+ notifyListener(Cr.NS_OK);
+ return;
+ } else if (response.notFound) {
+ cal.LOG(`CalDAV: Disabling calendar ${this.name} due to 404`);
+ notifyListener(Cr.NS_ERROR_FAILURE);
+ return;
+ } else if (response.ok && this.mDisabled) {
+ // Looks like the calendar is there again, check its resource
+ // type first.
+ this.checkDavResourceType(aChangeLogListener);
+ return;
+ } else if (!response.ok) {
+ cal.LOG("CalDAV: Failed to get ctag from server for calendar " + this.name);
+ notifyListener(Cr.NS_OK);
+ return;
+ }
+
+ let ctag = response.firstProps["CS:getctag"];
+ if (!ctag || ctag != this.mCtag) {
+ // ctag mismatch, need to fetch calendar-data
+ this.mProposedCtag = ctag;
+ this.getUpdatedItems(this.calendarUri, aChangeLogListener);
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: ctag mismatch on refresh, fetching data for calendar " + this.name);
+ }
+ } else {
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: ctag matches, no need to fetch data for calendar " + this.name);
+ }
+
+ // Notify the listener, but don't return just yet...
+ notifyListener(Cr.NS_OK);
+
+ // ...we may still need to poll the inbox
+ if (this.firstInRealm()) {
+ this.pollInbox();
+ }
+ }
+ });
+ },
+
+ refresh() {
+ this.replayChangesOn(null);
+ },
+
+ firstInRealm() {
+ 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;
+ },
+
+ /**
+ * Get updated items
+ *
+ * @param {nsIURI} aUri - The uri to request the items from.
+ * NOTE: This must be the uri without any uri
+ * params. They will be appended in this function.
+ * @param aChangeLogListener - (optional) The listener to notify for cached
+ * calendars.
+ */
+ getUpdatedItems(aUri, aChangeLogListener) {
+ if (this.mDisabled) {
+ // check if maybe our calendar has become available
+ this.checkDavResourceType(aChangeLogListener);
+ return;
+ }
+
+ if (this.mHasWebdavSyncSupport) {
+ let webDavSync = new CalDavWebDavSyncHandler(this, aUri, aChangeLogListener);
+ webDavSync.doWebDAVSync();
+ return;
+ }
+
+ let queryXml =
+ XML_HEADER +
+ '<D:propfind xmlns:D="DAV:">' +
+ "<D:prop>" +
+ "<D:getcontenttype/>" +
+ "<D:resourcetype/>" +
+ "<D:getetag/>" +
+ "</D:prop>" +
+ "</D:propfind>";
+
+ let requestUri = this.makeUri(null, aUri);
+ let handler = new CalDavEtagsHandler(this, aUri, aChangeLogListener);
+
+ let onSetupChannel = channel => {
+ channel.requestMethod = "PROPFIND";
+ channel.setRequestHeader("Depth", "1", false);
+ };
+ let request = new CalDavLegacySAXRequest(
+ this.session,
+ this,
+ requestUri,
+ queryXml,
+ MIME_TEXT_XML,
+ handler,
+ onSetupChannel
+ );
+
+ request.commit().catch(() => {
+ if (aChangeLogListener && this.isCached) {
+ aChangeLogListener.onResult(
+ { status: Cr.NS_ERROR_NOT_AVAILABLE },
+ Cr.NS_ERROR_NOT_AVAILABLE
+ );
+ }
+ });
+ },
+
+ /**
+ * @see nsIInterfaceRequestor
+ * @see calProviderUtils.jsm
+ */
+ getInterface: cal.provider.InterfaceRequestor_getInterface,
+
+ //
+ // Helper functions
+ //
+
+ /** Overriding lightning oauth **/
+
+ oauthConnect (authSuccessCb, authFailureCb, aRefresh = false) {
+ //unused
+ console.log("oauthConnect unused REPORT");
+ },
+
+ /**
+ * Called when a response has had its URL redirected. Shows a dialog
+ * to allow the user to accept or reject the redirect. If they accept,
+ * change the calendar's URI to the target URI of the redirect.
+ *
+ * @param {PropfindResponse} response - Response to handle. Typically a
+ * PropfindResponse but could be any
+ * subclass of CalDavResponseBase.
+ * @return {boolean} True if the user accepted the redirect.
+ * False, if the calendar should be disabled.
+ */
+ openUriRedirectDialog(response) {
+ let args = {
+ calendarName: this.name,
+ originalURI: response.nsirequest.originalURI.spec,
+ targetURI: response.uri.spec,
+ returnValue: false,
+ };
+
+ cal.window
+ .getCalendarWindow()
+ .openDialog(
+ "chrome://calendar/content/calendar-uri-redirect-dialog.xhtml",
+ "Calendar:URIRedirectDialog",
+ "chrome,modal,titlebar,resizable,centerscreen",
+ args
+ );
+
+ if (args.returnValue) {
+ this.uri = response.uri;
+ this.setProperty("uri", response.uri.spec);
+ }
+
+ return args.returnValue;
+ },
+
+ /**
+ * Checks that the calendar URI exists and is a CalDAV calendar. This is the beginning of a
+ * chain of asynchronous calls. This function will, when done, call the next function related to
+ * checking resource type, server capabilities, etc.
+ *
+ * checkDavResourceType * You are here
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ disableCalendars() {
+ this.setProperty("disabled", "true");
+ this.setProperty("auto-enabled", "true");
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_FAILURE);
+ },
+ 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);
+ });
+ },
+
+ async checkDavResourceType(aChangeLogListener) {
+ // 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.");
+ this.disableCalendars();
+ 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.");
+ this.disableCalendars();
+ return;
+ }
+
+ // Wait until master password has been entered (if needed)
+ while (!Services.logins.isLoggedIn) {
+ await this.sleep(1000);
+ }
+
+ this.ensureTargetCalendar();
+ let request = new CalDavPropfindRequest(this.session, this, this.makeUri(), [
+ "D:resourcetype",
+ "D:owner",
+ "D:current-user-principal",
+ "D:supported-report-set",
+ "C:supported-calendar-component-set",
+ "CS:getctag",
+ ]);
+
+ request.commit().then(
+ response => {
+ cal.LOG(`CalDAV: Status ${response.status} on initial PROPFIND for calendar ${this.name}`);
+
+ // If the URI was redirected, and the user rejects the redirect, disable the calendar.
+ if (response.redirected && !this.openUriRedirectDialog(response)) {
+ this.setProperty("disabled", "true");
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_ABORT);
+ return;
+ }
+
+ if (response.clientError) {
+ // 4xx codes, which is either an authentication failure or something like method not
+ // allowed. This is a failure worth disabling the calendar.
+ this.setProperty("disabled", "true");
+ this.setProperty("auto-enabled", "true");
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_ABORT);
+ return;
+ } else if (response.serverError) {
+ // 5xx codes, a server error. This could be a temporary failure, i.e a backend
+ // server being disabled.
+ cal.LOG(
+ "CalDAV: Server not available " +
+ request.responseStatus +
+ ", abort sync for calendar " +
+ this.name
+ );
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_ABORT);
+ return;
+ }
+
+ let wwwauth = request.getHeader("Authorization");
+ this.mAuthScheme = wwwauth ? wwwauth.split(" ")[0] : "none";
+
+ if (this.mUriParams) {
+ this.mAuthScheme = "Ticket";
+ }
+ cal.LOG(`CalDAV: Authentication scheme for ${this.name} is ${this.mAuthScheme}`);
+
+ // We only really need the authrealm for Digest auth since only Digest is going to time
+ // out on us
+ if (this.mAuthScheme == "Digest") {
+ let realmChop = wwwauth.split('realm="')[1];
+ this.mAuthRealm = realmChop.split('", ')[0];
+ cal.LOG("CalDAV: realm " + this.mAuthRealm);
+ }
+
+ if (!response.text || response.notFound) {
+ // No response, or the calendar no longer exists.
+ cal.LOG("CalDAV: Failed to determine resource type for" + this.name);
+ this.completeCheckServerInfo(aChangeLogListener, Ci.calIErrors.DAV_NOT_DAV);
+ return;
+ }
+
+ let multistatus = response.xml;
+ if (!multistatus) {
+ cal.LOG(`CalDAV: Failed to determine resource type for ${this.name}`);
+ this.completeCheckServerInfo(aChangeLogListener, Ci.calIErrors.DAV_NOT_DAV);
+ return;
+ }
+
+ // check for webdav-sync capability
+ // http://tools.ietf.org/html/draft-daboo-webdav-sync
+ if (response.firstProps["D:supported-report-set"]?.has("D:sync-collection")) {
+ cal.LOG("CalDAV: Collection has webdav sync support");
+ this.mHasWebdavSyncSupport = true;
+ }
+
+ // check for server-side ctag support only if webdav sync is not available
+ let ctag = response.firstProps["CS:getctag"];
+ if (!this.mHasWebdavSyncSupport && ctag) {
+ // We compare the stored ctag with the one we just got, if
+ // they don't match, we update the items in safeRefresh.
+ if (ctag == this.mCtag) {
+ this.mFirstRefreshDone = true;
+ }
+
+ this.mProposedCtag = ctag;
+ if (this.verboseLogging()) {
+ cal.LOG(`CalDAV: initial ctag ${ctag} for calendar ${this.name}`);
+ }
+ }
+
+ // Use supported-calendar-component-set if the server supports it; some do not.
+ let supportedComponents = response.firstProps["C:supported-calendar-component-set"];
+ if (supportedComponents.size) {
+ this.mSupportedItemTypes = [...this.mGenerallySupportedItemTypes].filter(itype => {
+ return supportedComponents.has(itype);
+ });
+ cal.LOG(
+ `Adding supported items: ${this.mSupportedItemTypes.join(",")} for calendar: ${
+ this.name
+ }`
+ );
+ }
+
+ // check if current-user-principal or owner is specified; might save some work finding
+ // the principal URL.
+ let owner = response.firstProps["D:owner"];
+ let cuprincipal = response.firstProps["D:current-user-principal"];
+ if (cuprincipal) {
+ this.mPrincipalUrl = cuprincipal;
+ cal.LOG(
+ "CalDAV: Found principal url from DAV:current-user-principal " + this.mPrincipalUrl
+ );
+ } else if (owner) {
+ this.mPrincipalUrl = owner;
+ cal.LOG("CalDAV: Found principal url from DAV:owner " + this.mPrincipalUrl);
+ }
+
+ let resourceType = response.firstProps["D:resourcetype"] || new Set();
+ if (resourceType.has("C:calendar")) {
+ // This is a valid calendar resource
+ if (this.mDisabled) {
+ this.mDisabled = false;
+ this.mReadOnly = false;
+ }
+ this.setCalHomeSet(true);
+ this.checkServerCaps(aChangeLogListener);
+ } else if (resourceType.has("D:collection")) {
+ // Not a CalDAV calendar
+ cal.LOG(`CalDAV: ${this.name} points to a DAV resource, but not a CalDAV calendar`);
+ this.completeCheckServerInfo(aChangeLogListener, Ci.calIErrors.DAV_DAV_NOT_CALDAV);
+ } else {
+ // Something else?
+ cal.LOG(
+ `CalDAV: No resource type received, ${this.name} doesn't seem to point to a DAV resource`
+ );
+ this.completeCheckServerInfo(aChangeLogListener, Ci.calIErrors.DAV_NOT_DAV);
+ }
+ },
+ e => {
+ cal.LOG(`CalDAV: Error during initial PROPFIND for calendar ${this.name}: ${e}`);
+ this.completeCheckServerInfo(aChangeLogListener, Ci.calIErrors.DAV_NOT_DAV);
+ }
+ );
+ },
+
+ /**
+ * Checks server capabilities.
+ *
+ * checkDavResourceType
+ * checkServerCaps * You are here
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ checkServerCaps(aChangeLogListener, calHomeSetUrlRetry) {
+ let request = new CalDavHeaderRequest(this.session, this, this.makeUri(null, this.mCalHomeSet));
+
+ request.commit().then(
+ response => {
+ if (!response.ok) {
+ if (!calHomeSetUrlRetry && response.notFound) {
+ // try again with calendar URL, see https://bugzilla.mozilla.org/show_bug.cgi?id=588799
+ cal.LOG(
+ "CalDAV: Calendar homeset was not found at parent url of calendar URL" +
+ ` while querying options ${this.name}, will try calendar URL itself now`
+ );
+ this.setCalHomeSet(false);
+ this.checkServerCaps(aChangeLogListener, true);
+ } else {
+ cal.LOG(
+ `CalDAV: Unexpected status ${response.status} while querying options ${this.name}`
+ );
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_FAILURE);
+ }
+
+ // No further processing needed, we have called subsequent (async) functions above.
+ return;
+ }
+
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: DAV features: " + [...response.features.values()].join(", "));
+ }
+
+ if (response.features.has("calendar-auto-schedule")) {
+ if (this.verboseLogging()) {
+ cal.LOG(`CalDAV: Calendar ${this.name} supports calendar-auto-schedule`);
+ }
+ this.mHasAutoScheduling = true;
+ // leave outbound inbox/outbox scheduling off
+ } else if (response.features.has("calendar-schedule")) {
+ if (this.verboseLogging()) {
+ cal.LOG(`CalDAV: Calendar ${this.name} generally supports calendar-schedule`);
+ }
+ this.hasScheduling = true;
+ }
+
+ if (this.hasAutoScheduling || response.features.has("calendar-schedule")) {
+ // XXX - we really shouldn't register with the fb service if another calendar with
+ // the same principal-URL has already done so. We also shouldn't register with the
+ // fb service if we don't have an outbox.
+ if (!this.hasFreeBusy) {
+ // This may have already been set by fetchCachedMetaData, we only want to add
+ // the freebusy provider once.
+ this.hasFreeBusy = true;
+ cal.getFreeBusyService().addProvider(this);
+ }
+ this.findPrincipalNS(aChangeLogListener);
+ } else {
+ cal.LOG("CalDAV: Server does not support CalDAV scheduling.");
+ this.completeCheckServerInfo(aChangeLogListener);
+ }
+ },
+ e => {
+ cal.LOG(`CalDAV: Error checking server capabilities for calendar ${this.name}: ${e}`);
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_FAILURE);
+ }
+ );
+ },
+
+ /**
+ * Locates the principal namespace. This function should solely be called
+ * from checkServerCaps to find the principal namespace.
+ *
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS * You are here
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo
+ */
+ findPrincipalNS(aChangeLogListener) {
+ if (this.principalUrl) {
+ // We already have a principal namespace, use it.
+ this.checkPrincipalsNameSpace([this.principalUrl], aChangeLogListener);
+ return;
+ }
+
+ let homeSet = this.makeUri(null, this.mCalHomeSet);
+ let request = new CalDavPropfindRequest(this.session, this, homeSet, [
+ "D:principal-collection-set",
+ ]);
+
+ request.commit().then(
+ response => {
+ if (response.ok) {
+ let pcs = response.firstProps["D:principal-collection-set"];
+ let nsList = pcs ? pcs.map(path => this.ensureDecodedPath(path)) : [];
+
+ this.checkPrincipalsNameSpace(nsList, aChangeLogListener);
+ } else {
+ cal.LOG(
+ "CalDAV: Unexpected status " +
+ response.status +
+ " while querying principal namespace for " +
+ this.name
+ );
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_FAILURE);
+ }
+ },
+ e => {
+ cal.LOG(`CalDAV: Failed to propstat principal namespace for calendar ${this.name}: ${e}`);
+ this.completeCheckServerInfo(aChangeLogListener, Cr.NS_ERROR_FAILURE);
+ }
+ );
+ },
+
+ /**
+ * Checks the principals namespace for scheduling info. This function should
+ * solely be called from findPrincipalNS
+ *
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace * You are here
+ * completeCheckServerInfo
+ *
+ * @param aNameSpaceList List of available namespaces
+ */
+ checkPrincipalsNameSpace(aNameSpaceList, aChangeLogListener) {
+ let doesntSupportScheduling = () => {
+ this.hasScheduling = false;
+ this.mInboxUrl = null;
+ this.mOutboxUrl = null;
+ this.completeCheckServerInfo(aChangeLogListener);
+ };
+
+ if (!aNameSpaceList.length) {
+ if (this.verboseLogging()) {
+ cal.LOG(
+ "CalDAV: principal namespace list empty, calendar " +
+ this.name +
+ " doesn't support scheduling"
+ );
+ }
+ doesntSupportScheduling();
+ return;
+ }
+
+ // We want a trailing slash, ensure it.
+ let nextNS = aNameSpaceList.pop().replace(/([^\/])$/, "$1/"); // eslint-disable-line no-useless-escape
+ let requestUri = Services.io.newURI(this.calendarUri.prePath + this.ensureEncodedPath(nextNS));
+ let requestProps = [
+ "C:calendar-home-set",
+ "C:calendar-user-address-set",
+ "C:schedule-inbox-URL",
+ "C:schedule-outbox-URL",
+ ];
+
+ let request;
+ if (this.mPrincipalUrl) {
+ request = new CalDavPropfindRequest(this.session, this, requestUri, requestProps);
+ } else {
+ let homePath = this.ensureEncodedPath(this.mCalHomeSet.spec.replace(/\/$/, ""));
+ request = new CalDavPrincipalPropertySearchRequest(
+ this.session,
+ this,
+ requestUri,
+ homePath,
+ "C:calendar-home-set",
+ requestProps
+ );
+ }
+
+ request.commit().then(
+ response => {
+ let homeSetMatches = homeSet => {
+ let normalized = homeSet.replace(/([^\/])$/, "$1/"); // eslint-disable-line no-useless-escape
+ let chs = this.mCalHomeSet;
+ return normalized == chs.path || normalized == chs.spec;
+ };
+ let createBoxUrl = path => {
+ let newPath = this.ensureDecodedPath(path);
+ // Make sure the uri has a / at the end, as we do with the calendarUri.
+ if (newPath.charAt(newPath.length - 1) != "/") {
+ newPath += "/";
+ }
+ return this.mUri
+ .mutate()
+ .setPathQueryRef(newPath)
+ .finalize();
+ };
+
+ if (!response.ok) {
+ cal.LOG(
+ `CalDAV: Bad response to in/outbox query, status ${response.status} for ${this.name}`
+ );
+ doesntSupportScheduling();
+ return;
+ }
+
+ // If there are multiple home sets, we need to match the email addresses for scheduling.
+ // If there is only one, assume its the right one.
+ // TODO with multiple address sets, we should just use the ACL manager.
+ let homeSets = response.firstProps["C:calendar-home-set"];
+ if (homeSets.length == 1 || homeSets.some(homeSetMatches)) {
+ for (let addr of response.firstProps["C:calendar-user-address-set"]) {
+ if (addr.match(/^mailto:/i)) {
+ this.mCalendarUserAddress = addr;
+ }
+ }
+
+ this.mInboxUrl = createBoxUrl(response.firstProps["C:schedule-inbox-URL"]);
+ this.mOutboxUrl = createBoxUrl(response.firstProps["C:schedule-outbox-URL"]);
+
+ if (this.calendarUri.spec == this.mInboxUrl.spec) {
+ // If the inbox matches the calendar uri (i.e SOGo), then we
+ // don't need to poll the inbox.
+ this.mShouldPollInbox = false;
+ }
+ }
+
+ if (!this.calendarUserAddress || !this.mInboxUrl || !this.mOutboxUrl) {
+ if (aNameSpaceList.length) {
+ // Check the next namespace to find the info we need.
+ this.checkPrincipalsNameSpace(aNameSpaceList, aChangeLogListener);
+ } else {
+ if (this.verboseLogging()) {
+ cal.LOG(
+ "CalDAV: principal namespace list empty, calendar " +
+ this.name +
+ " doesn't support scheduling"
+ );
+ }
+ doesntSupportScheduling();
+ }
+ } else {
+ // We have everything, complete.
+ this.completeCheckServerInfo(aChangeLogListener);
+ }
+ },
+ e => {
+ cal.LOG(`CalDAV: Failure checking principal namespace for calendar ${this.name}: ${e}`);
+ doesntSupportScheduling();
+ }
+ );
+ },
+
+ /**
+ * This is called to complete checking the server info. It should be the
+ * final call when checking server options. This will either report the
+ * error or if it is a success then refresh the calendar.
+ *
+ * checkDavResourceType
+ * checkServerCaps
+ * findPrincipalNS
+ * checkPrincipalsNameSpace
+ * completeCheckServerInfo * You are here
+ */
+ completeCheckServerInfo(aChangeLogListener, aError = Cr.NS_OK) {
+ if (Components.isSuccessCode(aError)) {
+ this.saveCalendarProperties();
+ this.checkedServerInfo = true;
+ this.setProperty("currentStatus", Cr.NS_OK);
+
+ if (this.isCached) {
+ this.safeRefresh(aChangeLogListener);
+ } else {
+ this.refresh();
+ }
+ } else {
+ this.reportDavError(aError);
+ if (this.isCached && aChangeLogListener) {
+ aChangeLogListener.onResult({ status: Cr.NS_ERROR_FAILURE }, Cr.NS_ERROR_FAILURE);
+ }
+ }
+ },
+
+ /**
+ * Called to report a certain DAV error. Strings and modification type are
+ * handled here.
+ */
+ reportDavError(aErrNo, status, extraInfo) {
+ let mapError = {};
+ mapError[Ci.calIErrors.DAV_NOT_DAV] = "dav_notDav";
+ mapError[Ci.calIErrors.DAV_DAV_NOT_CALDAV] = "dav_davNotCaldav";
+ mapError[Ci.calIErrors.DAV_PUT_ERROR] = "itemPutError";
+ mapError[Ci.calIErrors.DAV_REMOVE_ERROR] = "itemDeleteError";
+ mapError[Ci.calIErrors.DAV_REPORT_ERROR] = "disabledMode";
+
+ let mapModification = {};
+ mapModification[Ci.calIErrors.DAV_NOT_DAV] = false;
+ mapModification[Ci.calIErrors.DAV_DAV_NOT_CALDAV] = false;
+ mapModification[Ci.calIErrors.DAV_PUT_ERROR] = true;
+ mapModification[Ci.calIErrors.DAV_REMOVE_ERROR] = true;
+ mapModification[Ci.calIErrors.DAV_REPORT_ERROR] = false;
+
+ let message = mapError[aErrNo];
+ let localizedMessage;
+ let modificationError = mapModification[aErrNo];
+
+ if (!message) {
+ // Only notify if there is a message for this error
+ return;
+ }
+ localizedMessage = cal.l10n.getCalString(message, [this.mUri.spec]);
+ this.mReadOnly = true;
+ this.mDisabled = true;
+ this.notifyError(aErrNo, localizedMessage);
+ this.notifyError(
+ modificationError ? Ci.calIErrors.MODIFICATION_FAILED : Ci.calIErrors.READ_FAILED,
+ this.buildDetailedMessage(status, extraInfo)
+ );
+ },
+
+ buildDetailedMessage(status, extraInfo) {
+ if (!status) {
+ return "";
+ }
+
+ let props = Services.strings.createBundle("chrome://calendar/locale/calendar.properties");
+ let statusString;
+ try {
+ statusString = props.GetStringFromName("caldavRequestStatusCodeString" + status);
+ } catch (e) {
+ // Fallback on generic string if no string is defined for the status code
+ statusString = props.GetStringFromName("caldavRequestStatusCodeStringGeneric");
+ }
+ return (
+ props.formatStringFromName("caldavRequestStatusCode", [status]) +
+ ", " +
+ statusString +
+ "\n\n" +
+ (extraInfo ? extraInfo : "")
+ );
+ },
+
+ //
+ // calIFreeBusyProvider interface
+ //
+
+ getFreeBusyIntervals(aCalId, aRangeStart, aRangeEnd, aBusyTypes, aListener) {
+ // We explicitly don't check for hasScheduling here to allow free-busy queries
+ // even in case sched is turned off.
+ if (!this.outboxUrl || !this.calendarUserAddress) {
+ cal.LOG(
+ "CalDAV: Calendar " +
+ this.name +
+ " doesn't support scheduling;" +
+ " freebusy query not possible"
+ );
+ aListener.onResult(null, null);
+ return;
+ }
+
+ if (!this.firstInRealm()) {
+ // don't spam every known outbox with freebusy queries
+ aListener.onResult(null, null);
+ return;
+ }
+
+ // We tweak the organizer lookup here: If e.g. scheduling is turned off, then the
+ // configured email takes place being the organizerId for scheduling which need
+ // not match against the calendar-user-address:
+ let orgId = this.getProperty("organizerId");
+ if (orgId && orgId.toLowerCase() == aCalId.toLowerCase()) {
+ aCalId = this.calendarUserAddress; // continue with calendar-user-address
+ }
+
+ // the caller prepends MAILTO: to calid strings containing @
+ // but apple needs that to be mailto:
+ let aCalIdParts = aCalId.split(":");
+ aCalIdParts[0] = aCalIdParts[0].toLowerCase();
+ if (aCalIdParts[0] != "mailto" && aCalIdParts[0] != "http" && aCalIdParts[0] != "https") {
+ aListener.onResult(null, null);
+ return;
+ }
+
+ let organizer = this.calendarUserAddress;
+ let recipient = aCalIdParts.join(":");
+ let fbUri = this.makeUri(null, this.outboxUrl);
+
+ let request = new CalDavFreeBusyRequest(
+ this.session,
+ this,
+ fbUri,
+ organizer,
+ recipient,
+ aRangeStart,
+ aRangeEnd
+ );
+
+ request.commit().then(
+ response => {
+ if (!response.xml || response.status != 200) {
+ cal.LOG(
+ "CalDAV: Received status " + response.status + " from freebusy query for " + this.name
+ );
+ aListener.onResult(null, null);
+ return;
+ }
+
+ let fbTypeMap = {
+ UNKNOWN: Ci.calIFreeBusyInterval.UNKNOWN,
+ FREE: Ci.calIFreeBusyInterval.FREE,
+ BUSY: Ci.calIFreeBusyInterval.BUSY,
+ "BUSY-UNAVAILABLE": Ci.calIFreeBusyInterval.BUSY_UNAVAILABLE,
+ "BUSY-TENTATIVE": Ci.calIFreeBusyInterval.BUSY_TENTATIVE,
+ };
+
+ let status = response.firstRecipient.status;
+ if (!status || !status.startsWith("2")) {
+ cal.LOG(`CalDAV: Got status ${status} in response to freebusy query for ${this.name}`);
+ aListener.onResult(null, null);
+ return;
+ }
+
+ if (!status.startsWith("2.0")) {
+ cal.LOG(`CalDAV: Got status ${status} in response to freebusy query for ${this.name}`);
+ }
+
+ let intervals = response.firstRecipient.intervals.map(data => {
+ let fbType = fbTypeMap[data.type] || Ci.calIFreeBusyInterval.UNKNOWN;
+ return new cal.provider.FreeBusyInterval(aCalId, fbType, data.begin, data.end);
+ });
+
+ aListener.onResult(null, intervals);
+ },
+ e => {
+ cal.LOG(`CalDAV: Failed freebusy request for ${this.name}: ${e}`);
+ aListener.onResult(null, null);
+ }
+ );
+ },
+
+ /**
+ * Extract the path from the full spec, if the regexp failed, log
+ * warning and return unaltered path.
+ */
+ extractPathFromSpec(aSpec) {
+ // The parsed array should look like this:
+ // a[0] = full string
+ // a[1] = scheme
+ // a[2] = everything between the scheme and the start of the path
+ // a[3] = extracted path
+ let a = aSpec.match("(https?)(://[^/]*)([^#?]*)");
+ if (a && a[3]) {
+ return a[3];
+ }
+ cal.WARN("CalDAV: Spec could not be parsed, returning as-is: " + aSpec);
+ return aSpec;
+ },
+ /**
+ * This is called to create an encoded path from a unencoded path OR
+ * encoded full url
+ *
+ * @param aString {string} un-encoded path OR encoded uri spec.
+ */
+ ensureEncodedPath(aString) {
+ if (aString.charAt(0) != "/") {
+ aString = this.ensureDecodedPath(aString);
+ }
+ let uriComponents = aString.split("/");
+ uriComponents = uriComponents.map(encodeURIComponent);
+ return uriComponents.join("/");
+ },
+
+ /**
+ * This is called to get a decoded path from an encoded path or uri spec.
+ *
+ * @param {string} aString - Represents either a path
+ * or a full uri that needs to be decoded.
+ * @return {string} A decoded path.
+ */
+ ensureDecodedPath(aString) {
+ if (aString.charAt(0) != "/") {
+ aString = this.extractPathFromSpec(aString);
+ }
+
+ let uriComponents = aString.split("/");
+ for (let i = 0; i < uriComponents.length; i++) {
+ try {
+ uriComponents[i] = decodeURIComponent(uriComponents[i]);
+ } catch (e) {
+ cal.WARN("CalDAV: Exception decoding path " + aString + ", segment: " + uriComponents[i]);
+ }
+ }
+ return uriComponents.join("/");
+ },
+ isInbox(aString) {
+ // Note: If you change this, make sure it really returns a boolean
+ // value and not null!
+ return (
+ (this.hasScheduling || this.hasAutoScheduling) &&
+ this.mInboxUrl != null &&
+ aString.startsWith(this.mInboxUrl.spec)
+ );
+ },
+
+ /**
+ * Query contents of scheduling inbox
+ *
+ */
+ pollInbox() {
+ // If polling the inbox was switched off, no need to poll the inbox.
+ // Also, if we have more than one calendar in this CalDAV account, we
+ // want only one of them to be checking the inbox.
+ if (
+ (!this.hasScheduling && !this.hasAutoScheduling) ||
+ !this.mShouldPollInbox ||
+ !this.firstInRealm()
+ ) {
+ return;
+ }
+
+ this.getUpdatedItems(this.mInboxUrl, null);
+ },
+
+ //
+ // take calISchedulingSupport interface base implementation (cal.provider.BaseClass)
+ //
+
+ processItipReply(aItem, aPath) {
+ // modify partstat for in-calendar item
+ // delete item from inbox
+ let self = this;
+
+ let getItemListener = {};
+ getItemListener.QueryInterface = ChromeUtils.generateQI([Ci.calIOperationListener]);
+ getItemListener.onOperationComplete = function(
+ aCalendar,
+ aStatus,
+ aOperationType,
+ aId,
+ aDetail
+ ) {};
+ getItemListener.onGetResult = function(aCalendar, aStatus, aItemType, aDetail, aItems) {
+ let itemToUpdate = aItems[0];
+ if (aItem.recurrenceId && itemToUpdate.recurrenceInfo) {
+ itemToUpdate = itemToUpdate.recurrenceInfo.getOccurrenceFor(aItem.recurrenceId);
+ }
+ let newItem = itemToUpdate.clone();
+
+ for (let attendee of aItem.getAttendees()) {
+ let att = newItem.getAttendeeById(attendee.id);
+ if (att) {
+ newItem.removeAttendee(att);
+ att = att.clone();
+ att.participationStatus = attendee.participationStatus;
+ newItem.addAttendee(att);
+ }
+ }
+ self.doModifyItem(
+ newItem,
+ itemToUpdate.parentItem /* related to bug 396182 */,
+ modListener,
+ true
+ );
+ };
+
+ let modListener = {};
+ modListener.QueryInterface = ChromeUtils.generateQI([Ci.calIOperationListener]);
+ modListener.onOperationComplete = function(
+ aCalendar,
+ aStatus,
+ aOperationType,
+ aItemId,
+ aDetail
+ ) {
+ cal.LOG(`CalDAV: status ${aStatus} while processing iTIP REPLY for ${self.name}`);
+ // don't delete the REPLY item from inbox unless modifying the master
+ // item was successful
+ if (aStatus == 0) {
+ // aStatus undocumented; 0 seems to indicate no error
+ let delUri = self.calendarUri
+ .mutate()
+ .setPathQueryRef(self.ensureEncodedPath(aPath))
+ .finalize();
+ self.doDeleteItem(aItem, null, true, true, delUri);
+ }
+ };
+
+ this.mOfflineStorage.getItem(aItem.id, getItemListener);
+ },
+
+ canNotify(aMethod, aItem) {
+ // canNotify should return false if the imip transport should takes care of notifying cal
+ // users
+ if (this.getProperty("forceEmailScheduling")) {
+ return false;
+ }
+ if (this.hasAutoScheduling || this.hasScheduling) {
+ // we go with server's scheduling capabilities here - we take care for exceptions if
+ // schedule agent is set to CLIENT in sendItems()
+ switch (aMethod) {
+ // supported methods as per RfC 6638
+ case "REPLY":
+ case "REQUEST":
+ case "CANCEL":
+ case "ADD":
+ return true;
+ default:
+ cal.LOG(
+ "Not supported method " +
+ aMethod +
+ " detected - falling back to email based scheduling."
+ );
+ }
+ }
+ return false; // use outbound iTIP for all
+ },
+
+ //
+ // calIItipTransport interface
+ //
+
+ get scheme() {
+ return "mailto";
+ },
+
+ mSenderAddress: null,
+ get senderAddress() {
+ return this.mSenderAddress || this.calendarUserAddress;
+ },
+ set senderAddress(aString) {
+ return (this.mSenderAddress = aString);
+ },
+
+ sendItems(aRecipients, aItipItem) {
+ function doImipScheduling(aCalendar, aRecipientList) {
+ let result = false;
+ let imipTransport = cal.provider.getImipTransport(aCalendar);
+ let recipients = [];
+ aRecipientList.forEach(rec => recipients.push(rec.toString()));
+ if (imipTransport) {
+ cal.LOG(
+ "Enforcing client-side email scheduling instead of server-side scheduling" +
+ " for " +
+ recipients.join()
+ );
+ result = imipTransport.sendItems(aRecipientList, aItipItem);
+ } else {
+ cal.ERROR(
+ "No imip transport available for " +
+ aCalendar.id +
+ ", failed to notify" +
+ recipients.join()
+ );
+ }
+ return result;
+ }
+
+ if (this.getProperty("forceEmailScheduling")) {
+ return doImipScheduling(this, aRecipients);
+ }
+
+ if (this.hasAutoScheduling || this.hasScheduling) {
+ // let's make sure we notify calendar users marked for client-side scheduling by email
+ let recipients = [];
+ for (let item of aItipItem.getItemList()) {
+ if (aItipItem.receivedMethod == "REPLY") {
+ if (item.organizer.getProperty("SCHEDULE-AGENT") == "CLIENT") {
+ recipients.push(item.organizer);
+ }
+ } else {
+ let atts = item.getAttendees().filter(att => {
+ return att.getProperty("SCHEDULE-AGENT") == "CLIENT";
+ });
+ for (let att of atts) {
+ recipients.push(att);
+ }
+ }
+ }
+ if (recipients.length) {
+ // We return the imip scheduling status here as any remaining calendar user will be
+ // notified by the server without Lightning receiving a status in the first place.
+ // We maybe could inspect the scheduling status of those attendees when
+ // re-retriving the modified event and try to do imip schedule on any status code
+ // other then 1.0, 1.1 or 1.2 - but I leave without that for now.
+ return doImipScheduling(this, recipients);
+ }
+ return true;
+ }
+
+ // from here on this code for explicit caldav scheduling
+ if (aItipItem.responseMethod == "REPLY") {
+ // Get my participation status
+ let attendee = aItipItem.getItemList()[0].getAttendeeById(this.calendarUserAddress);
+ if (!attendee) {
+ return false;
+ }
+ // work around BUG 351589, the below just removes RSVP:
+ aItipItem.setAttendeeStatus(attendee.id, attendee.participationStatus);
+ }
+
+ for (let item of aItipItem.getItemList()) {
+ let requestUri = this.makeUri(null, this.outboxUrl);
+ let request = new CalDavOutboxRequest(
+ this.session,
+ this,
+ requestUri,
+ this.calendarUserAddress,
+ aRecipients,
+ item
+ );
+
+ request.commit().then(
+ response => {
+ if (!response.ok) {
+ cal.LOG(`CalDAV: Sending iTIP failed with status ${response.status} for ${this.name}`);
+ }
+
+ let lowerRecipients = new Map(aRecipients.map(recip => [recip.id.toLowerCase(), recip]));
+ let remainingAttendees = [];
+ for (let [recipient, status] of Object.entries(response.data)) {
+ if (status.startsWith("2")) {
+ continue;
+ }
+
+ let att = lowerRecipients.get(recipient.toLowerCase());
+ if (att) {
+ remainingAttendees.push(att);
+ }
+ }
+
+ if (this.verboseLogging()) {
+ cal.LOG(
+ "CalDAV: Failed scheduling delivery to " +
+ remainingAttendees.map(att => att.id).join(", ")
+ );
+ }
+
+ if (remainingAttendees.length) {
+ // try to fall back to email delivery if CalDAV-sched didn't work
+ let imipTransport = cal.provider.getImipTransport(this);
+ if (imipTransport) {
+ if (this.verboseLogging()) {
+ cal.LOG(`CalDAV: sending email to ${remainingAttendees.length} recipients`);
+ }
+ imipTransport.sendItems(remainingAttendees, aItipItem);
+ } else {
+ cal.LOG("CalDAV: no fallback to iTIP/iMIP transport for " + this.name);
+ }
+ }
+ },
+ e => {
+ cal.LOG(`CalDAV: Failed itip request for ${this.name}: ${e}`);
+ }
+ );
+ }
+ return true;
+ },
+
+ mVerboseLogging: undefined,
+ verboseLogging() {
+ if (this.mVerboseLogging === undefined) {
+ this.mVerboseLogging = Services.prefs.getBoolPref("calendar.debug.log.verbose", false);
+ }
+ return this.mVerboseLogging;
+ },
+
+ getSerializedItem(aItem) {
+ let serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance(
+ Ci.calIIcsSerializer
+ );
+ serializer.addItems([aItem]);
+ let serializedItem = serializer.serializeToString();
+ if (this.verboseLogging()) {
+ cal.LOG("CalDAV: send: " + serializedItem);
+ }
+ return serializedItem;
+ },
+};
+
+function calDavObserver(aCalendar) {
+ this.mCalendar = aCalendar;
+}
+
+calDavObserver.prototype = {
+ mCalendar: null,
+ mInBatch: false,
+
+ // calIObserver:
+ onStartBatch() {
+ this.mCalendar.observers.notify("onStartBatch");
+ this.mInBatch = true;
+ },
+ onEndBatch() {
+ this.mCalendar.observers.notify("onEndBatch");
+ this.mInBatch = false;
+ },
+ onLoad(calendar) {
+ this.mCalendar.observers.notify("onLoad", [calendar]);
+ },
+ onAddItem(aItem) {
+ this.mCalendar.observers.notify("onAddItem", [aItem]);
+ },
+ onModifyItem(aNewItem, aOldItem) {
+ this.mCalendar.observers.notify("onModifyItem", [aNewItem, aOldItem]);
+ },
+ onDeleteItem(aDeletedItem) {
+ this.mCalendar.observers.notify("onDeleteItem", [aDeletedItem]);
+ },
+ onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
+ this.mCalendar.observers.notify("onPropertyChanged", [aCalendar, aName, aValue, aOldValue]);
+ },
+ onPropertyDeleting(aCalendar, aName) {
+ this.mCalendar.observers.notify("onPropertyDeleting", [aCalendar, aName]);
+ },
+
+ onError(aCalendar, aErrNo, aMessage) {
+ this.mCalendar.readOnly = true;
+ this.mCalendar.notifyError(aErrNo, aMessage);
+ },
+};
diff --git a/content/includes/GoogleDavSession.jsm b/content/includes/GoogleDavSession.jsm
new file mode 100644
index 0000000..8d4599c
--- /dev/null
+++ b/content/includes/GoogleDavSession.jsm
@@ -0,0 +1,132 @@
+/* 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 { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+/**
+ * Session and authentication tools for the googledav provider
+ */
+
+const EXPORTED_SYMBOLS = ["GoogleDavSession"]; /* exported GoogleDavSession */
+let sessions = {};
+
+/**
+ * A session for the googledav provider. Two or more calendars can share a session if they have the
+ * same auth credentials.
+ */
+class GoogleDavSession {
+ QueryInterface(aIID) {
+ return cal.generateClassQI(this, aIID, [Ci.nsIInterfaceRequestor]);
+ }
+
+ /**
+ * Constant returned by |completeRequest| when the request should be restarted
+ * @return {Number} The constant
+ */
+ static get RESTART_REQUEST() {
+ return 1;
+ }
+
+ /**
+ * Creates a new googledav session
+ *
+ * @param {String} aSessionId The session id, used in the password manager
+ * @param {String} aName The user-readable description of this session
+ */
+ constructor(aSessionId, aName) {
+ this.id = aSessionId;
+ this.name = aName;
+ }
+
+ /**
+ * Implement nsIInterfaceRequestor. The base class has no extra interfaces, but a subclass of
+ * the session may.
+ *
+ * @param {nsIIDRef} aIID The IID of the interface being requested
+ * @return {?*} Either this object QI'd to the IID, or null.
+ * Components.returnCode is set accordingly.
+ */
+ getInterface(aIID) {
+ try {
+ // Try to query the this object for the requested interface but don't
+ // throw if it fails since that borks the network code.
+ return this.QueryInterface(aIID);
+ } catch (e) {
+ Components.returnCode = e;
+ }
+
+ return null;
+ }
+
+ /**
+ * Calls the auth adapter for the given host in case it exists. This allows delegating auth
+ * preparation based on the host, e.g. for OAuth.
+ *
+ * @param {String} aURI The uri to check the auth adapter for
+ * @param {String} aMethod The method to call
+ * @param {...*} aArgs Remaining args specific to the adapted method
+ * @return {*} Return value specific to the adapter method
+ */
+ getSession(aURI) {
+ // accountID is stored as username
+ let accountID = aURI.username || null;
+ if (!accountID)
+ return null;
+
+ if (!sessions.hasOwnProperty(accountID)) {
+ sessions[accountID] = TbSync.providers.dav.network.getOAuthObj(aURI);
+ }
+ return sessions[accountID];
+ }
+
+ /**
+ * Prepare the channel for a request, e.g. setting custom authentication headers
+ *
+ * @param {nsIChannel} aChannel The channel to prepare
+ * @return {Promise} A promise resolved when the preparations are complete
+ */
+ async prepareRequest(aChannel) {
+ let session = this.getSession(aChannel.URI);
+ if (!session)
+ return null;
+
+ try {
+ if (!session.accessToken || session.isExpired()) {
+ if (!(await session.asyncConnect({}))) {
+ return null;
+ }
+ }
+ aChannel.setRequestHeader("Authorization", "Bearer " + session.accessToken, false);
+ } catch(e) {
+ Components.utils.reportError(e);
+ }
+ }
+
+ /**
+ * Prepare the given new channel for a redirect, e.g. copying headers.
+ *
+ * @param {nsIChannel} aOldChannel The old channel that is being redirected
+ * @param {nsIChannel} aNewChannel The new channel to prepare
+ * @return {Promise} A promise resolved when the preparations are complete
+ */
+ async prepareRedirect(aOldChannel, aNewChannel) {
+ //console.log("prepareRedirect");
+ //https://searchfox.org/comm-esr78/source/calendar/providers/caldav/modules/CalDavSession.jsm#192
+ }
+
+ /**
+ * Complete the request based on the results from the response. Allows restarting the session if
+ * |GoogleDavSession.RESTART_REQUEST| is returned.
+ *
+ * @param {CalDavResponseBase} aResponse The response to inspect for completion
+ * @return {Promise} A promise resolved when complete, with
+ * GoogleDavSession.RESTART_REQUEST or null
+ */
+ async completeRequest(aResponse) {
+ //console.log("completeRequest");
+ //https://searchfox.org/comm-esr78/source/calendar/providers/caldav/modules/CalDavSession.jsm#214
+ }
+}
diff --git a/content/includes/abUI.js b/content/includes/abUI.js
new file mode 100644
index 0000000..c512899
--- /dev/null
+++ b/content/includes/abUI.js
@@ -0,0 +1,476 @@
+/*
+ * 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/content/skin/type.pref.png");
+ } else {
+ icon.setAttribute("src", "chrome://dav4tbsync/content/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/content/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/content/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/content/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.setAttribute("style", "padding:0; margin:0");
+ 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/content/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("style", "width: 35px; min-width: 35px; margin: 0; padding:0");
+ button.appendChild(aDocument.getElementById("DavMenuTemplate").children[0].cloneNode(true));
+ outerhbox.appendChild(button);
+
+ //email box
+ let emailbox = aDocument.createXULElement("hbox");
+ emailbox.setAttribute("flex", "1");
+ let email = aDocument.createElement("input");
+ email.setAttribute("style", "width: 240px");
+ 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.setAttribute("style", "padding:0;margin:0;");
+ 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/content/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/content/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/content/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.setAttribute("style", "padding:0; margin:0");
+ 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/content/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("style", "width: 35px; min-width: 35px; margin: 0; padding:0");
+ button1.appendChild(aDocument.getElementById("DavMenuTemplate").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("DavMenuTemplate").children[2].cloneNode(true));
+ outerhbox.appendChild(button2);
+
+ //phone box
+ let phonebox = aDocument.createXULElement("hbox");
+ phonebox.setAttribute("flex", "1");
+ let phone = aDocument.createElement("input");
+ phone.setAttribute("style", "width: 205px");
+ 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.setAttribute("style", "padding:0;margin:0;");
+ 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;
+ }
+ },
+
+}
diff --git a/content/includes/network.js b/content/includes/network.js
new file mode 100644
index 0000000..bbad0a4
--- /dev/null
+++ b/content/includes/network.js
@@ -0,0 +1,649 @@
+/*
+ * 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 : dav.sync.prefSettings.getCharPref("OAuth2_ClientID"),
+ client_secret : dav.sync.prefSettings.getCharPref("OAuth2_ClientSecret"),
+ }
+ break;
+
+ default:
+ return null;
+ }
+
+ let oauth = new OAuth2(config.base_uri + "oauth2/auth", config.base_uri + "oauth2/token", 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);
+ });
+ }
+}
diff --git a/content/includes/sync.js b/content/includes/sync.js
new file mode 100644
index 0000000..8fa055c
--- /dev/null
+++ b/content/includes/sync.js
@@ -0,0 +1,910 @@
+/*
+/*
+ * 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("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-principal /></d:prop></d:propfind>", 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 = "<d:propfind "+dav.tools.xmlns(["d", job, "cs"])+"><d:prop><"+job+":" + homeset + " />"
+ + (job == "cal" && options.includes("calendar-proxy") ? "<cs:calendar-proxy-write-for /><cs:calendar-proxy-read-for />" : "")
+ + "<d:group-membership />"
+ + "</d:prop></d:propfind>";
+
+ 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("<d:group-membership />",""), 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")
+ ? "<d:propfind "+dav.tools.xmlns(["d","apple","cs"])+"><d:prop><d:current-user-privilege-set/><d:resourcetype /><d:displayname /><apple:calendar-color/><cs:source/></d:prop></d:propfind>"
+ : "<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-privilege-set/><d:resourcetype /><d:displayname /></d:prop></d:propfind>";
+
+ //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.
+ if (color && job == "cal") {
+ 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
+ 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("<d:sync-collection "+dav.tools.xmlns(["d"])+"><d:sync-token>"+token+"</d:sync-token><d:sync-level>1</d:sync-level><d:prop><d:getetag/></d:prop></d:sync-collection>", 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 = await 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("<d:propfind "+dav.tools.xmlns(["d", "cs"])+"><d:prop><cs:getctag /><d:sync-token /></d:prop></d:propfind>", 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("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:getetag /></d:prop></d:propfind>", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
+
+ //to test other impl
+ //let cards = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:getetag /></d:prop></d:propfind>", 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("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:getetag /><d:getcontenttype /></d:prop></d:propfind>", syncData.currentFolderData.getFolderProperty("href"), "PROPFIND", syncData.connectionData, {"Depth": "1", "Prefer": "return=minimal"});
+
+ //play with filters and limits for synology
+ /*
+ let additional = "<card:limit><card:nresults>10</card:nresults></card:limit>";
+ additional += "<card:filter test='anyof'>";
+ additional += "<card:prop-filter name='FN'>";
+ additional += "<card:text-match negate-condition='yes' match-type='equals'>bogusxy</card:text-match>";
+ additional += "</card:prop-filter>";
+ additional += "</card:filter>";*/
+
+ //addressbook-query does not work on older servers (zimbra)
+ //let cards = await dav.network.sendRequest("<card:addressbook-query "+dav.tools.xmlns(["d", "card"])+"><d:prop><d:getetag /></d:prop></card:addressbook-query>", 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 = await 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 = await syncData.target.getItemFromProperty("X-DAV-HREF", listID);
+ if (!list.isMailList)
+ continue;
+
+ //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),
+ // and which one was added (in new but not in old)
+ let removedMembers = oCardInfo.members.filter(e => !vCardInfo.members.includes(e));
+ let newMembers = vCardInfo.members.filter(e => !oCardInfo.members.includes(e));
+
+ // Check that all new members have an email address (fix for bug 1522453)
+ let m=0;
+ for (let member of newMembers) {
+ let card = await 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++;
+ }
+
+ // if any of the to-be-removed members are not members of the local list, they are skipt
+ // if any of the to-be-added members are already members of the local list, they are skipt
+ list.removeListMembers("X-DAV-UID", removedMembers);
+ list.addListMembers("X-DAV-UID", newMembers);
+ syncData.target.modifyItem(list);
+ }
+ }
+ }
+ },
+
+ 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 = await 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 = await 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);
+ },
+}
diff --git a/content/includes/tools.js b/content/includes/tools.js
new file mode 100644
index 0000000..a281df1
--- /dev/null
+++ b/content/includes/tools.js
@@ -0,0 +1,1301 @@
+/*
+ * 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], "");
+ if (email) {
+ if (emailData.length > i) emailData[i].value = email.trim();
+ else emailData.push({value: email.trim(), meta: []});
+ }
+ }
+
+ return emailData;
+ },
+
+ getPhoneNumbersFromCard: function (aCard) { //return array of objects {meta, value}
+ let phones = [];
+ try {
+ phones = JSON.parse(aCard.getProperty("X-DAV-JSON-Phones","").trim());
+ return phones;
+ } catch (e) {
+ //Components.utils.reportError(e);
+ }
+
+ //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, "");
+ if (phone) {
+ phones.push({value: phone.trim(), 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) {
+ // prepare defaults
+ let emailFields = {PrimaryEmail:[], SecondEmail:[]};
+
+ 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);
+
+ 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 phoneMap = [
+ {meta: "CELL", field: "CellularNumber"},
+ {meta: "FAX", field: "FaxNumber"},
+ {meta: "PAGER", field: "PagerNumber"},
+ {meta: "WORK", field: "WorkPhone"},
+ {meta: "", field: "HomePhone"},
+ ];
+
+ // prepare defaults
+ let phoneFields = {};
+ for (let m=0; m < phoneMap.length; m++) {
+ phoneFields[phoneMap[m].field] = [];
+ }
+
+ 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);
+
+ for (let m=0; m < phoneMap.length; m++) {
+ 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 = "<card:addressbook-multiget "+dav.tools.xmlns(["d", "card"])+"><d:prop><d:getetag /><card:address-data /></d:prop>";
+ let counts = 0;
+ for (let i=0; i < hrefs.length; i++) {
+ request += "<d:href>"+hrefs[i]+"</d:href>";
+ counts++;
+ }
+ request += "</card:addressbook-multiget>";
+
+ 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 (!(await 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);
+ await 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 = await syncData.target.getItemFromProperty("X-DAV-HREF", id);
+ if (card) {
+ //check if contact or mailinglist to update card
+ if (!(await 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: async 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);
+ await 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));
+ TbSync.dump("JSON from oCard", oCardData ? 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 {
+ // Try to get the type from the the meta field but only use a given subtype (cut of leading "image/").
+ // See draft: https://tools.ietf.org/id/draft-ietf-vcarddav-vcardrev-02.html#PHOTO
+ // However, no mentioning of this in final RFC2426 for vCard 3.0.
+ // Also make sure, that the final type does not include any non alphanumeric chars.
+ type = vCardData[vCardField.item][0].meta.type[0].toLowerCase().split("/").pop().replace(/\W/g, "");
+ } 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) {
+
+ let ext = type || "jpg";
+ let data = vCardData[vCardField.item][0].value;
+ card.addPhoto(TbSync.generateUUID(), data, ext);
+ } 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 ext = type || this.getImageExtension(vCardData[vCardField.item][0].value);
+ let data = await dav.network.sendRequest("", vCardData[vCardField.item][0].value , "GET", connectionData, {}, {responseType: "base64"});
+ card.addPhoto(TbSync.generateUUID(), data, ext, vCardData[vCardField.item][0].value);
+ } catch(e) {
+ Components.utils.reportError(e);
+ TbSync.eventlog.add("warning", syncData.eventLogInfo,"Could not extract externally linked photo from vCard", JSON.stringify(vCardData));
+ }
+ }
+ } else {
+ //clear
+ card.setProperty("PhotoName", "");
+ card.setProperty("PhotoType", "");
+ card.setProperty("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.setProperty("BirthYear", "");
+ card.setProperty("BirthMonth", "");
+ card.setProperty("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.setProperty(field, "");
+ }
+ }
+ }
+ }
+
+ default:
+ {
+ if (newServerValue) {
+ //set
+ card.setProperty(property, newServerValue);
+ } else {
+ //clear
+ card.setProperty(property, "");
+ }
+ }
+ 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 = "jpg";
+ try {
+ let parts = filename.toString().split("/").pop().split(".");
+ let lastPart = parts.pop();
+ if (parts.length > 0 && lastPart) {
+ extension = lastPart;
+ }
+ } catch (e) {}
+ return extension.toLowerCase();
+ },
+
+
+ //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.toUpperCase();
+
+ 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));
+ } else {
+ dav.tools.updateValueOfVCard(syncData, property, vCardData, vCardField, "");
+ }
+ }
+ 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");
+ 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", 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", 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", 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};
+ },
+
+}
diff --git a/content/includes/vcard/LICENSE b/content/includes/vcard/LICENSE
new file mode 100644
index 0000000..fa9dba1
--- /dev/null
+++ b/content/includes/vcard/LICENSE
@@ -0,0 +1,21 @@
+MIT 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.
diff --git a/content/includes/vcard/SOURCE b/content/includes/vcard/SOURCE
new file mode 100644
index 0000000..21d88d2
--- /dev/null
+++ b/content/includes/vcard/SOURCE
@@ -0,0 +1 @@
+https://github.com/Heymdall/vcard/releases/tag/v0.2.7 \ No newline at end of file
diff --git a/content/includes/vcard/vcard.js b/content/includes/vcard/vcard.js
new file mode 100644
index 0000000..d1b9757
--- /dev/null
+++ b/content/includes/vcard/vcard.js
@@ -0,0 +1,305 @@
+(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);
diff --git a/content/locales.js b/content/locales.js
new file mode 100644
index 0000000..a8beca0
--- /dev/null
+++ b/content/locales.js
@@ -0,0 +1,3 @@
+var { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+TbSync.localizeOnLoad(window, "dav");
diff --git a/content/manager/createAccount.js b/content/manager/createAccount.js
new file mode 100644
index 0000000..0c74cf9
--- /dev/null
+++ b/content/manager/createAccount.js
@@ -0,0 +1,559 @@
+/*
+ * 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) {
+ if (p == "google" && !dav.sync.prefSettings.getBoolPref("googlesupport"))
+ continue;
+ 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/content/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("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:current-user-principal /></d:prop></d:propfind>", 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;
+ // bug https://bugzilla.mozilla.org/show_bug.cgi?id=1618252
+ document.getElementById('tbsync.newaccount.wizard')._adjustWizardHeader();
+ 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();
+ }
+ },
+};
diff --git a/content/manager/createAccount.xhtml b/content/manager/createAccount.xhtml
new file mode 100644
index 0000000..eefb4ce
--- /dev/null
+++ b/content/manager/createAccount.xhtml
@@ -0,0 +1,123 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window
+ width="500"
+ height="600"
+ onload="tbSyncDavNewAccount.onLoad();"
+ onunload="tbSyncDavNewAccount.onUnload();"
+ onclose="return tbSyncDavNewAccount.onClose()"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <linkset>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl"/>
+ </linkset>
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/manager/createAccount.js"/>
+ <script type="application/javascript" src="chrome://dav4tbsync/content/locales.js"/>
+
+ <wizard
+ title="__DAV4TBSYNCMSG_add.title__"
+ id="tbsync.newaccount.wizard">
+
+ <wizardpage id="firstPage" onFirstPage="true" label="__DAV4TBSYNCMSG_add.serverprofile.title__">
+ <vbox flex="1">
+ <description style="width: 450px; margin-top:1em;">__DAV4TBSYNCMSG_add.serverprofile.description__</description>
+ <richlistbox id="tbsync.newaccount.serviceproviderlist" flex="1" seltype="single" style="margin-top:1ex" onselect="tbSyncDavNewAccount.clearValues();" ondblclick="tbSyncDavNewAccount.advance()"/>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="secondPage" label="__DAV4TBSYNCMSG_add.data.title__">
+ <vbox flex="1">
+ <description style="width: 450px; margin-top:1em;">__DAV4TBSYNCMSG_add.data.description__</description>
+ <grid style="margin-top:1ex">
+ <columns>
+ <column flex="1" />
+ <column flex="2" />
+ </columns>
+ <rows>
+ <row id="tbsync.newaccount.name.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.name__" /></vbox>
+ <html:input id="tbsync.newaccount.name" oninput="tbSyncDavNewAccount.checkUI();"/>
+ </row>
+ <row id="tbsync.newaccount.user.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.user__" /></vbox>
+ <html:input id="tbsync.newaccount.user" oninput="tbSyncDavNewAccount.checkUI(true);" />
+ </row>
+ <row id="tbsync.newaccount.password.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.password__" /></vbox>
+ <html:input id="tbsync.newaccount.password" type="password" oninput="tbSyncDavNewAccount.checkUI(true);"/>
+ </row>
+ <row id="tbsync.newaccount.server.row" style="margin-top:2em;">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.server__" /></vbox>
+ <html:input id="tbsync.newaccount.server" oninput="tbSyncDavNewAccount.checkUI(true);"/>
+ </row>
+ <row id="tbsync.newaccount.caldavserver.row" style="margin-top:2em;">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.caldavserver__" /></vbox>
+ <html:input id="tbsync.newaccount.caldavserver" oninput="tbSyncDavNewAccount.checkUI(true);"/>
+ </row>
+ <row id="tbsync.newaccount.carddavserver.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.carddavserver__" /></vbox>
+ <html:input id="tbsync.newaccount.carddavserver" oninput="tbSyncDavNewAccount.checkUI(true);"/>
+ </row>
+ </rows>
+ </grid>
+ <label class="header" id="tbsync.newaccount.details.header" value="__DAV4TBSYNCMSG_add.data.notes__" style="margin-top:2em" />
+ <description id="tbsync.newaccount.details1" style="width: 450px; margin-top:1ex;"></description>
+ <description id="tbsync.newaccount.details2" style="width: 450px; margin-top:1ex;"></description>
+ <description id="tbsync.newaccount.details3" style="width: 450px; margin-top:1ex;"></description>
+ <vbox flex="1">
+ </vbox>
+ <hbox id="tbsync.spinner">
+ <label id="tbsync.spinner.label" value="" />
+ <image src="chrome://tbsync/content/skin/spinner.gif" style="margin-left:1em" width="16" height="16"/>
+ </hbox>
+ <vbox id="tbsync.error" style="width: 450px;">
+ <description id="tbsync.error.message" flex="1" style="font-weight: bold;"></description>
+ <vbox>
+ <button
+ id="tbsync.error.link"
+ label="__DAV4TBSYNCMSG_manager.ShowEventLog__"
+ oncommand="TbSync.eventlog.open();"/>
+ </vbox>
+ </vbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="thirdPage" label="__DAV4TBSYNCMSG_add.finish.title__">
+ <vbox flex="1">
+ <description style="width: 450px; margin-top:1em;">__DAV4TBSYNCMSG_add.finish.description__</description>
+ <grid style="margin-top:1ex">
+ <columns>
+ <column flex="1" />
+ <column flex="2" />
+ </columns>
+ <rows>
+ <row>
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.name__" /></vbox>
+ <html:input id="tbsync.finalaccount.name" style="margin:2px" />
+ </row>
+ <row id="tbsync.newaccount.finaluser.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.user__" /></vbox>
+ <html:input type="text" id="tbsync.finalaccount.user" readonly="true" style="margin:2px; background-color:silver;" />
+ </row>
+ <row style="margin-bottom:2em;">
+ </row>
+ <row id="tbsyncfinalaccount.caldavserver.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.caldavserver__" /></vbox>
+ <html:input id="tbsync.finalaccount.caldavserver" readonly="true" style="margin:2px; background-color:silver;" />
+ </row>
+ <row id="tbsyncfinalaccount.carddavserver.row">
+ <vbox pack="center"><label value="__DAV4TBSYNCMSG_add.carddavserver__" /></vbox>
+ <html:input id="tbsync.finalaccount.carddavserver" readonly="true" style="margin:2px; background-color:silver;" />
+ </row>
+ </rows>
+ </grid>
+ <description id="tbsync.finalaccount.details1" style="width: 450px; margin-top:2em;">__DAV4TBSYNCMSG_add.finish.details__</description>
+ </vbox>
+ </wizardpage>
+
+ </wizard>
+
+</window>
diff --git a/content/manager/editAccountOverlay.js b/content/manager/editAccountOverlay.js
new file mode 100644
index 0000000..8f32af8
--- /dev/null
+++ b/content/manager/editAccountOverlay.js
@@ -0,0 +1,46 @@
+/*
+ * 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";
+
+const dav = TbSync.providers.dav;
+
+var tbSyncEditAccountOverlay = {
+
+ onload: function (window, accountData) {
+ this.accountData = accountData;
+
+ let serviceprovider = this.accountData.getAccountProperty("serviceprovider");
+ let isServiceProvider = dav.sync.serviceproviders.hasOwnProperty(serviceprovider);
+
+ // special treatment for configuration label, which is a permanent setting and will not change by switching modes
+ let configlabel = window.document.getElementById("tbsync.accountsettings.label.config");
+ if (configlabel) {
+ let extra = "";
+ if (isServiceProvider) {
+ extra = " [" + TbSync.getString("add.serverprofile." + serviceprovider, "dav") + "]";
+ }
+ configlabel.setAttribute("value", TbSync.getString("config.custom", "dav") + extra);
+ }
+
+ //set certain elements as "alwaysDisable", if locked by service provider
+ if (isServiceProvider) {
+ let items = window.document.getElementsByClassName("lockIfServiceProvider");
+ for (let i=0; i < items.length; i++) {
+ items[i].setAttribute("alwaysDisabled", "true");
+ }
+ }
+ },
+
+ stripHost: function (document, field) {
+ let host = document.getElementById('tbsync.accountsettings.pref.' + field).value;
+ while (host.endsWith("/")) { host = host.slice(0,-1); }
+ document.getElementById('tbsync.accountsettings.pref.' + field).value = host
+ this.accountData.setAccountProperty(field, host);
+ }
+};
diff --git a/content/manager/editAccountOverlay.xhtml b/content/manager/editAccountOverlay.xhtml
new file mode 100644
index 0000000..9b20ba7
--- /dev/null
+++ b/content/manager/editAccountOverlay.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://tbsync/content/skin/fix_dropdown_1534697.css" type="text/css"?>
+<overlay
+ id="tbSyncAccountConfig"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/manager/editAccountOverlay.js"/>
+ <script type="application/javascript" src="chrome://dav4tbsync/content/locales.js"/>
+
+ <tab id="manager.tabs.accountsettings" label="__DAV4TBSYNCMSG_manager.tabs.accountsettings__" appendto="manager.tabs" />
+ <tab id="manager.tabs.syncsettings" label="__DAV4TBSYNCMSG_manager.tabs.syncsettings__" appendto="manager.tabs" />
+
+ <tabpanel id="manager.tabpanels.accountsettings" appendto="manager.tabpanels" flex="1" orient="vertical"><!-- ACCOUNT SETTINGS -->
+ <vbox flex="1">
+ <label class="header lockIfConnected" style="margin-left:0; margin-bottom:1ex;" value="" id="tbsync.accountsettings.label.config" />
+ <grid flex="1">
+ <columns>
+ <column flex="0" style="margin-right:1ex;" />
+ <column flex="1" />
+ </columns>
+
+ <rows>
+ <row>
+ <vbox pack="center">
+ <label style="text-align:left" control="tbsync.accountsettings.pref.accountname" value="__DAV4TBSYNCMSG_pref.AccountName__" />
+ </vbox>
+ <html:input id="tbsync.accountsettings.pref.accountname" />
+ </row>
+
+ <row>
+ <vbox pack="center">
+ <label class="lockIfConnected" style="text-align:left" control="tbsync.accountsettings.pref.user" value="__DAV4TBSYNCMSG_pref.UserName__" />
+ </vbox>
+ <html:input class="lockIfConnected" id="tbsync.accountsettings.pref.user" />
+ </row>
+
+ <row>
+ <vbox pack="center">
+ <label class="lockIfConnected lockIfServiceProvider" style="text-align:left" control="tbsync.accountsettings.pref.calDavHost" value="__DAV4TBSYNCMSG_pref.CalDavServer__" />
+ </vbox>
+ <html:input class="lockIfConnected lockIfServiceProvider" id="tbsync.accountsettings.pref.calDavHost" onblur="tbSyncEditAccountOverlay.stripHost(document, 'calDavHost');"/>
+ </row>
+ <row>
+ <vbox pack="center">
+ <label class="lockIfConnected lockIfServiceProvider" style="text-align:left" control="tbsync.accountsettings.pref.cardDavHost" value="__DAV4TBSYNCMSG_pref.CardDavServer__" />
+ </vbox>
+ <html:input class="lockIfConnected lockIfServiceProvider" id="tbsync.accountsettings.pref.cardDavHost" onblur="tbSyncEditAccountOverlay.stripHost(document, 'cardDavHost');" />
+ </row>
+
+ </rows>
+ </grid>
+
+ <vbox flex="1" />
+ <vbox class="showIfConnected">
+ <hbox>
+ <vbox pack="center"><image src="chrome://tbsync/content/skin/info16.png" /></vbox>
+ <description flex="1">__TBSYNCMSG_manager.lockedsettings.description__</description>
+ </hbox>
+ </vbox>
+
+ </vbox>
+ </tabpanel>
+
+ <tabpanel id="manager.tabpanels.syncsettings" appendto="manager.tabpanels" flex="1" orient="vertical"><!-- SYNC SETTINGS -->
+ <vbox flex="1">
+ <!--label style="margin-left:0; margin-bottom: 1ex;" class="header lockIfConnected" value="__DAV4TBSYNCMSG_pref.generaloptions__"/-->
+
+ <label style="margin-left:0; margin-bottom: 1ex;" class="header lockIfConnected" value="__DAV4TBSYNCMSG_pref.contactoptions__"/>
+ <vbox>
+ <checkbox class="lockIfConnected" id="tbsync.accountsettings.pref.syncGroups" label="__DAV4TBSYNCMSG_pref.syncGroups__" />
+ <description style="margin-left:2em" class="lockIfConnected">__DAV4TBSYNCMSG_pref.syncGroupsDescription__</description>
+ <checkbox class="lockIfConnected" id="tbsync.accountsettings.pref.useCardBook" hidden="true" label="__DAV4TBSYNCMSG_pref.useCardBook__" />
+ </vbox>
+
+ <label style="margin-left:0; margin-bottom: 1ex; margin-top: 3ex" class="header lockIfConnected" value="__DAV4TBSYNCMSG_pref.calendaroptions__"/>
+ <vbox>
+ <checkbox class="lockIfConnected" id="tbsync.accountsettings.pref.useCalendarCache" label="__DAV4TBSYNCMSG_pref.useCalendarCache__" />
+ </vbox>
+
+ <vbox flex="1" />
+ <vbox class="showIfConnected">
+ <hbox>
+ <vbox pack="center"><image src="chrome://tbsync/content/skin/info16.png" /></vbox>
+ <description flex="1">__TBSYNCMSG_manager.lockedsettings.description__</description>
+ </hbox>
+ </vbox>
+ </vbox>
+ </tabpanel>
+
+</overlay>
diff --git a/content/overlays/abCSS.xhtml b/content/overlays/abCSS.xhtml
new file mode 100644
index 0000000..ed136b9
--- /dev/null
+++ b/content/overlays/abCSS.xhtml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://dav4tbsync/content/skin/ab.css" type="text/css"?>
+
+<overlay
+ id="TbSyncAbDavCssOverlay"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+</overlay>
diff --git a/content/overlays/abCardWindow.js b/content/overlays/abCardWindow.js
new file mode 100644
index 0000000..f4b2163
--- /dev/null
+++ b/content/overlays/abCardWindow.js
@@ -0,0 +1,143 @@
+/*
+ * This file is part of 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+var { OS } =ChromeUtils.import("resource://gre/modules/osfile.jsm");
+
+var tbSyncAbDavCardWindow = {
+
+ onBeforeInject: function (window) {
+ let aParentDirURI = "";
+
+ if (window.location.href=="chrome://messenger/content/addressbook/abNewCardDialog.xhtml") {
+ //get provider via uri from drop down
+ aParentDirURI = window.document.getElementById("abPopup").value;
+ } else {
+ //function to get correct uri of current card for global book as well for mailLists
+ aParentDirURI = TbSync.providers.dav.ui.getSelectedUri(window.arguments[0].abURI, window.arguments[0].card);
+ }
+
+ //returning false will prevent injection
+ return (TbSync.addressbook.getStringValue(MailServices.ab.getDirectory(aParentDirURI), "tbSyncProvider", "") == "dav");
+ },
+
+ onInject: function (window) {
+ //keep track of default elements we hide/disable, so it can be undone during overlay remove
+ tbSyncAbDavCardWindow.elementsToHide = [];
+ tbSyncAbDavCardWindow.elementsToDisable = [];
+
+ //register default elements we need to hide/disable
+ tbSyncAbDavCardWindow.elementsToHide.push(window.document.getElementById("PrimaryEmailLabel"));
+ tbSyncAbDavCardWindow.elementsToHide.push(window.document.getElementById("PrimaryEmail"));
+ tbSyncAbDavCardWindow.elementsToHide.push(window.document.getElementById("SecondEmailLabel"));
+ tbSyncAbDavCardWindow.elementsToHide.push(window.document.getElementById("SecondEmail"));
+ tbSyncAbDavCardWindow.elementsToHide.push(window.document.getElementById("PhoneNumbers"));
+
+ //hide stuff from gContactSync *grrrr* - I cannot hide all because he adds them via javascript :-(
+ tbSyncAbDavCardWindow.elementsToHide.push(window.document.getElementById("gContactSyncTab"));
+
+ //hide registered default elements
+ for (let i=0; i < tbSyncAbDavCardWindow.elementsToHide.length; i++) {
+ if (tbSyncAbDavCardWindow.elementsToHide[i]) {
+ console.log(tbSyncAbDavCardWindow.elementsToHide[i].style.display)
+ tbSyncAbDavCardWindow.elementsToHide[i].style.display = "none";
+ }
+ }
+
+ //disable registered default elements
+ for (let i=0; i < tbSyncAbDavCardWindow.elementsToDisable.length; i++) {
+ if (tbSyncAbDavCardWindow.elementsToDisable[i]) {
+ tbSyncAbDavCardWindow.elementsToDisable[i].disabled = true;
+ }
+ }
+
+ if (window.location.href=="chrome://messenger/content/addressbook/abNewCardDialog.xhtml") {
+ window.RegisterSaveListener(tbSyncAbDavCardWindow.onSaveCard);
+ } else {
+ window.RegisterLoadListener(tbSyncAbDavCardWindow.onLoadCard);
+ window.RegisterSaveListener(tbSyncAbDavCardWindow.onSaveCard);
+
+ //if this window was open during inject, load the extra fields
+ if (gEditCard) tbSyncAbDavCardWindow.onLoadCard(gEditCard.card, window.document);
+ }
+ TbSync.localizeNow(window, "dav");
+ window.sizeToContent();
+ },
+
+ onRemove: function (window) {
+ if (window.location.href=="chrome://messenger/content/addressbook/abNewCardDialog.xhtml") {
+ window.UnregisterSaveListener(tbSyncAbDavCardWindow.onSaveCard);
+ } else {
+ window.UnregisterLoadListener(tbSyncAbDavCardWindow.onLoadCard);
+ window.UnregisterSaveListener(tbSyncAbDavCardWindow.onSaveCard);
+ }
+
+ //unhide elements hidden by this provider
+ for (let i=0; i < tbSyncAbDavCardWindow.elementsToHide.length; i++) {
+ if (tbSyncAbDavCardWindow.elementsToHide[i]) {
+ tbSyncAbDavCardWindow.elementsToHide[i].style.display = "";
+ }
+ }
+
+ //re-enable elements disabled by this provider
+ for (let i=0; i < tbSyncAbDavCardWindow.elementsToDisable.length; i++) {
+ if (tbSyncAbDavCardWindow.elementsToDisable[i]) {
+ tbSyncAbDavCardWindow.elementsToDisable[i].disabled = false;
+ }
+ }
+
+ },
+
+ onLoadCard: function (aCard, aDocument) {
+ //load properties
+ let items = aDocument.getElementsByClassName("davProperty");
+ for (let i=0; i < items.length; i++) {
+ items[i].value = aCard.getProperty(items[i].id, "");
+ }
+
+ //get all emails with metadata from card
+ let emails = TbSync.providers.dav.tools.getEmailsFromCard(aCard); //array of objects {meta, value}
+ //add emails to list
+ let emailList = aDocument.getElementById("X-DAV-EmailAddressList");
+ if (emailList) {
+ for (let i=0; i < emails.length; i++) {
+ let item = TbSync.providers.dav.ui.getNewEmailListItem(aDocument, emails[i]);
+ emailList.appendChild(item);
+
+ TbSync.providers.dav.ui.updateType(aDocument, TbSync.providers.dav.ui.getEmailListItemElement(item, "button"));
+ TbSync.providers.dav.ui.updatePref(aDocument, TbSync.providers.dav.ui.getEmailListItemElement(item, "pref"));
+ }
+ }
+
+ //get all phone numbers with metadata from card
+ let phones = TbSync.providers.dav.tools.getPhoneNumbersFromCard(aCard); //array of objects {meta, value}
+ //add phones to list
+ let phoneList = aDocument.getElementById("X-DAV-PhoneNumberList");
+ if (phoneList) {
+ for (let i=0; i < phones.length; i++) {
+ let item = TbSync.providers.dav.ui.getNewPhoneListItem(aDocument, phones[i]);
+ phoneList.appendChild(item);
+
+ TbSync.providers.dav.ui.updateType(aDocument, TbSync.providers.dav.ui.getPhoneListItemElement(item, "button1"));
+ TbSync.providers.dav.ui.updateType(aDocument, TbSync.providers.dav.ui.getPhoneListItemElement(item, "button2"));
+ TbSync.providers.dav.ui.updatePref(aDocument, TbSync.providers.dav.ui.getPhoneListItemElement(item, "pref"));
+ }
+ }
+
+ },
+
+ onSaveCard: function (aCard, aDocument) {
+ let items = aDocument.getElementsByClassName("davProperty");
+ for (let i=0; i < items.length; i++) {
+ aCard.setProperty(items[i].id, items[i].value);
+ }
+ }
+
+}
diff --git a/content/overlays/abCardWindow.xhtml b/content/overlays/abCardWindow.xhtml
new file mode 100644
index 0000000..0237ab6
--- /dev/null
+++ b/content/overlays/abCardWindow.xhtml
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://tbsync/content/skin/fix_dropdown_1534697.css" type="text/css"?>
+
+<overlay
+ id="TbSyncAbDavCardWindow"
+ omscope="tbSyncAbDavCardWindow"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/overlays/abCardWindow.js" />
+
+ <hbox id="DavMenuTemplate" insertbefore="contactGrid">
+ <menupopup position="start_before">
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.home__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'HOME')" image="chrome://dav4tbsync/content/skin/type.home16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.work__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'WORK')" image="chrome://dav4tbsync/content/skin/type.work16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.other__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'OTHER')" image="chrome://dav4tbsync/content/skin/type.other16.png" />
+ </menupopup>
+ <menupopup position="start_before">
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.home__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'HOME')" image="chrome://dav4tbsync/content/skin/type.home16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.work__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'WORK')" image="chrome://dav4tbsync/content/skin/type.work16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.other__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'OTHER')" image="chrome://dav4tbsync/content/skin/type.none16.png" />
+ </menupopup>
+ <menupopup position="start_before">
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.voice__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'VOICE')" image="chrome://dav4tbsync/content/skin/type.voice16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.cell__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'CELL')" image="chrome://dav4tbsync/content/skin/type.cell16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.fax__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'FAX')" image="chrome://dav4tbsync/content/skin/type.fax16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.video__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'VIDEO')" image="chrome://dav4tbsync/content/skin/type.video16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.pager__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'PAGER')" image="chrome://dav4tbsync/content/skin/type.pager16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.car__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'CAR')" image="chrome://dav4tbsync/content/skin/type.car16.png" />
+ <menuitem class="menuitem-iconic" label="__DAV4TBSYNCMSG_abCard.emailtypes.other__" oncommand="TbSync.providers.dav.ui.updateType(window.document, this.parentNode.parentNode, 'OTHER')" image="chrome://dav4tbsync/content/skin/type.none16.png" />
+ </menupopup>
+ </hbox>
+
+
+
+
+
+ <button id="jbdav1" style="visibility:hidden" label="+" insertbefore="nameFieldLabel"/>
+ <label id="jbdav2" class="align-end" value="__DAV4TBSYNCMSG_abCard.ContactDetails__" insertbefore="nameFieldLabel"/>
+
+ <label id="jbdav3" class="align-end" control="X-DAV-PrefixName" value="__DAV4TBSYNCMSG_abCard.PrefixName__" insertbefore="nameFieldLabel"/>
+ <hbox id="DavPrefixNameContainer" align="center" class="input-container" insertbefore="nameFieldLabel">
+ <html:input id="X-DAV-PrefixName" type="text" class="davProperty input-inline" />
+ </hbox>
+
+ <label id="jbdav4" class="align-end" control="X-DAV-MiddleName" value="__DAV4TBSYNCMSG_abCard.MiddleName__" insertbefore="nameField2Label"/>
+ <hbox id="DavMiddleNameContainer" align="center" class="input-container" insertbefore="nameField2Label">
+ <html:input id="X-DAV-MiddleName" type="text" class="davProperty input-inline" />
+ </hbox>
+
+ <label id="jbdav5" class="align-end" control="X-DAV-SuffixName" value="__DAV4TBSYNCMSG_abCard.SuffixName__" insertbefore="DisplayNameLabel"/>
+ <hbox id="DavSufixNameContainer" align="center" class="input-container" insertbefore="DisplayNameLabel">
+ <html:input id="X-DAV-SuffixName" type="text" class="davProperty input-inline" />
+ </hbox>
+
+
+
+ <vbox id="DavFieldsContainer" insertafter="PhoneNumbers">
+ <hbox flex="1">
+ <spacer style="width:3em;" />
+ <vbox class="CardEditWidth" style="width:350px">
+ <hbox align="center">
+ <label value="__DAV4TBSYNCMSG_abCard.Phone__" />
+ <spacer flex="1" />
+ <button id="DavPhoneAdd" label="+" oncommand="TbSync.providers.dav.ui.addPhoneEntry(window.document)" flex="0" style="width:3em; min-width:3em" />
+ </hbox>
+ <richlistbox flex="0" style="height:20ex;" id="X-DAV-PhoneNumberList" seltype="single" >
+ </richlistbox>
+ <!--label id="X-DAV-HomePhoneMetaInfo" collapsed="true" class="davProperty" />
+ <label id="X-DAV-WorkPhoneMetaInfo" collapsed="true" class="davProperty" /-->
+ </vbox>
+ </hbox>
+
+ <hbox flex="1" style="margin-top:1em">
+ <spacer style="width:3em;" />
+ <vbox class="CardEditWidth" style="width:350px">
+ <hbox align="center">
+ <label value="__DAV4TBSYNCMSG_abCard.EmailAddresses__" />
+ <spacer flex="1" />
+ <button id="DavEmailAdd" label="+" oncommand="TbSync.providers.dav.ui.addEmailEntry(window.document)" flex="0" style="width:3em; min-width:3em" />
+ </hbox>
+ <richlistbox flex="0" style="height:20ex;" id="X-DAV-EmailAddressList" seltype="single" >
+ </richlistbox>
+
+ <description style="width:350px">__DAV4TBSYNCMSG_abCard.emailtypes.description__</description>
+ <label id="X-DAV-JSON-Emails" collapsed="true" class="davProperty" />
+ <label id="X-DAV-JSON-Phones" collapsed="true" class="davProperty" />
+ </vbox>
+ </hbox>
+
+ </vbox>
+</overlay>
diff --git a/content/overlays/abNewCardWindow.js b/content/overlays/abNewCardWindow.js
new file mode 100644
index 0000000..2bf0d43
--- /dev/null
+++ b/content/overlays/abNewCardWindow.js
@@ -0,0 +1,30 @@
+/*
+ * This file is part of 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncDavAbNewCardWindow = {
+
+ onInject: function (window) {
+ window.document.getElementById("abPopup").addEventListener("select", tbSyncDavAbNewCardWindow.onAbSelectChangeNewCard, false);
+ },
+
+ onRemove: function (window) {
+ window.document.getElementById("abPopup").removeEventListener("select", tbSyncDavAbNewCardWindow.onAbSelectChangeNewCard, false);
+ },
+
+ onAbSelectChangeNewCard: function () {
+ //remove our overlay (if injected)
+ TbSync.providers.dav.overlayManager.removeOverlay(window, "chrome://dav4tbsync/content/overlays/abCardWindow.xhtml");
+ //inject our overlay (if our card)
+ TbSync.providers.dav.overlayManager.injectOverlay(window, "chrome://dav4tbsync/content/overlays/abCardWindow.xhtml");
+ },
+
+}
diff --git a/content/overlays/abNewCardWindow.xhtml b/content/overlays/abNewCardWindow.xhtml
new file mode 100644
index 0000000..0222615
--- /dev/null
+++ b/content/overlays/abNewCardWindow.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+
+<overlay
+ id="TbSyncDavAbNewCardWindowOverlay"
+ omscope="tbSyncDavAbNewCardWindow"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/overlays/abNewCardWindow.js" />
+ <script type="application/javascript" src="chrome://dav4tbsync/content/locales.js"/>
+
+</overlay>
+
+
+
diff --git a/content/overlays/addressbookdetailsoverlay.js b/content/overlays/addressbookdetailsoverlay.js
new file mode 100644
index 0000000..b11f1c8
--- /dev/null
+++ b/content/overlays/addressbookdetailsoverlay.js
@@ -0,0 +1,136 @@
+/*
+ * This file is part of 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncDavAddressBookDetails = {
+
+ onBeforeInject: function (window) {
+ //we inject always now and let onAbResultSelectionChanged handle our custom display
+ return true;
+ },
+
+ onInject: function (window) {
+ if (window.document.getElementById("abResultsTree")) {
+ window.document.getElementById("abResultsTree").addEventListener("select", tbSyncDavAddressBookDetails.onAbResultSelectionChanged, false);
+ tbSyncDavAddressBookDetails.onAbResultSelectionChanged();
+ }
+ TbSync.localizeNow(window, "dav");
+ },
+
+ onRemove: function (window) {
+ tbSyncDavAddressBookDetails.undoChangesToDefaults();
+ if (window.document.getElementById("abResultsTree")) {
+ window.document.getElementById("abResultsTree").removeEventListener("select", tbSyncDavAddressBookDetails.onAbResultSelectionChanged, false);
+ }
+ },
+
+ undoChangesToDefaults: function () {
+ //unhide elements hidden by this provider
+ if (tbSyncDavAddressBookDetails.hasOwnProperty("elementsToHide")) {
+ for (let i=0; i < tbSyncDavAddressBookDetails.elementsToHide.length; i++) {
+ if (tbSyncDavAddressBookDetails.elementsToHide[i]) {
+ tbSyncDavAddressBookDetails.elementsToHide[i].hidden = false;
+ }
+ }
+ }
+
+ //re-enable elements disabled by this provider
+ if (tbSyncDavAddressBookDetails.hasOwnProperty("elementsToDisable")) {
+ for (let i=0; i < tbSyncDavAddressBookDetails.elementsToDisable.length; i++) {
+ if (tbSyncDavAddressBookDetails.elementsToDisable[i]) {
+ tbSyncDavAddressBookDetails.elementsToDisable[i].disabled = false;
+ }
+ }
+ }
+
+ tbSyncDavAddressBookDetails.elementsToHide = [];
+ tbSyncDavAddressBookDetails.elementsToDisable = [];
+ },
+
+ onAbResultSelectionChanged: function () {
+ tbSyncDavAddressBookDetails.undoChangesToDefaults();
+
+ let cards = window.GetSelectedAbCards();
+ if (cards.length == 1) {
+ let aCard = cards[0];
+
+ //function to get correct uri of current card for global book as well for mailLists
+ let abUri = TbSync.providers.dav.ui.getSelectedUri(window.GetSelectedDirectory(), aCard);
+ if (TbSync.addressbook.getStringValue(MailServices.ab.getDirectory(abUri), "tbSyncProvider", "") != "dav") {
+ window.document.getElementById("cvbEmails").collapsed = true;
+ window.document.getElementById("cvbPhoneNumbers").collapsed =true;
+ return;
+ }
+
+ //add emails
+ let emails = TbSync.providers.dav.tools.getEmailsFromCard(aCard); //array of objects {meta, value}
+ let emailDetails = window.document.getElementById("cvbEmailRows");
+ if (emailDetails) {
+ //remove all rows
+ while (emailDetails.firstChild) {
+ emailDetails.removeChild(emailDetails.firstChild);
+ }
+
+ for (let i=0; i < emails.length; i++) {
+ emailDetails.appendChild(TbSync.providers.dav.ui.getNewEmailDetailsRow(window, emails[i]));
+ }
+
+ if (window.document.getElementById("cvbEmails")) {
+ window.document.getElementById("cvbEmails").collapsed = (emails.length == 0);
+ }
+ }
+
+ //add phone numbers
+ let phones = TbSync.providers.dav.tools.getPhoneNumbersFromCard(aCard); //array of objects {meta, value}
+ let phoneDetails = window.document.getElementById("cvbPhoneRows");
+ if (phoneDetails) {
+ //remove all rows
+ while (phoneDetails.firstChild) {
+ phoneDetails.removeChild(phoneDetails.firstChild);
+ }
+
+ for (let i=0; i < phones.length; i++) {
+ phoneDetails.appendChild(TbSync.providers.dav.ui.getNewPhoneDetailsRow(window, phones[i]));
+ }
+
+ if (window.document.getElementById("cvbPhoneNumbers")) {
+ window.document.getElementById("cvbPhoneNumbers").collapsed = (phones.length == 0);
+ }
+ }
+
+
+ //hide primary and secondary email
+ if (!tbSyncDavAddressBookDetails.hasOwnProperty("elementsToHide")) tbSyncDavAddressBookDetails.elementsToHide = [];
+ if (!tbSyncDavAddressBookDetails.hasOwnProperty("elementsToDisable")) tbSyncDavAddressBookDetails.elementsToDisable = [];
+ tbSyncDavAddressBookDetails.elementsToHide.push(window.document.getElementById("cvEmail1Box"));
+ tbSyncDavAddressBookDetails.elementsToHide.push(window.document.getElementById("cvEmail2Box"));
+ tbSyncDavAddressBookDetails.elementsToHide.push(window.document.getElementById("cvbPhone"));
+
+ //hide registered default elements
+ for (let i=0; i < tbSyncDavAddressBookDetails.elementsToHide.length; i++) {
+ if (tbSyncDavAddressBookDetails.elementsToHide[i]) {
+ tbSyncDavAddressBookDetails.elementsToHide[i].hidden = true;
+ //using "hidden" and not "collapsed", because TB is flipping collapsed itself after the card has been edited/saved
+ //and if we also use that property, the fields "blink" for a split second. Using "hidden" the field stays hidden even if TB is uncollapsing
+ }
+ }
+
+ //disable registered default elements
+ for (let i=0; i < tbSyncDavAddressBookDetails.elementsToDisable.length; i++) {
+ if (tbSyncDavAddressBookDetails.elementsToDisable[i]) {
+ tbSyncDavAddressBookDetails.elementsToDisable[i].disabled = true;
+ }
+ }
+
+ }
+ },
+
+}
diff --git a/content/overlays/addressbookdetailsoverlay.xhtml b/content/overlays/addressbookdetailsoverlay.xhtml
new file mode 100644
index 0000000..2d2a7b1
--- /dev/null
+++ b/content/overlays/addressbookdetailsoverlay.xhtml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+
+<overlay
+ id="TbSyncAbDavDetailsOverlay"
+ omscope="tbSyncDavAddressBookDetails"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/overlays/addressbookdetailsoverlay.js" />
+
+ <vbox id="cvbEmails" class="cardViewGroup" insertafter="cvbContact">
+ <description class="CardViewHeading" id="cvhEmails">__DAV4TBSYNCMSG_abCard.EmailAddresses__</description>
+ <grid>
+ <columns>
+ <column flex="0"/>
+ <column flex="1"/>
+ </columns>
+ <rows id="cvbEmailRows">
+ </rows>
+ </grid>
+ </vbox>
+
+ <vbox id="cvbPhoneNumbers" class="cardViewGroup" insertafter="cvbPhone">
+ <description class="CardViewHeading" id="cvhPhoneNumbers">__DAV4TBSYNCMSG_abCard.Phone__</description>
+ <grid>
+ <columns>
+ <column flex="0"/>
+ <column flex="1"/>
+ </columns>
+ <rows id="cvbPhoneRows">
+ </rows>
+ </grid>
+ </vbox>
+
+</overlay>
diff --git a/content/overlays/addressbookoverlay.js b/content/overlays/addressbookoverlay.js
new file mode 100644
index 0000000..d9dff4b
--- /dev/null
+++ b/content/overlays/addressbookoverlay.js
@@ -0,0 +1,42 @@
+/*
+ * This file is part of 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 { TbSync } = ChromeUtils.import("chrome://tbsync/content/tbsync.jsm");
+
+var tbSyncDavAddressBook = {
+
+ onInject: function (window) {
+ Services.obs.addObserver(tbSyncDavAddressBook.onAddressBookCreated, "tbsync.observer.addressbook.created", false);
+ if (window.document.getElementById("dirTree")) {
+ window.document.getElementById("dirTree").addEventListener("select", tbSyncDavAddressBook.onAbDirectorySelectionChanged, false);
+ }
+ },
+
+ onRemove: function (window) {
+ Services.obs.removeObserver(tbSyncDavAddressBook.onAddressBookCreated, "tbsync.observer.addressbook.created");
+ if (window.document.getElementById("dirTree")) {
+ window.document.getElementById("dirTree").removeEventListener("select", tbSyncDavAddressBook.onAbDirectorySelectionChanged, false);
+ }
+ },
+
+ onAddressBookCreated: {
+ observe: function (aSubject, aTopic, aData) {
+ tbSyncDavAddressBook.onAbDirectorySelectionChanged();
+ }
+ },
+
+ onAbDirectorySelectionChanged: function () {
+ //TODO: Do not do this, if provider did not change
+ //remove our details injection (if injected)
+ TbSync.providers.dav.overlayManager.removeOverlay(window, "chrome://dav4tbsync/content/overlays/addressbookdetailsoverlay.xhtml");
+ //inject our details injection (if the new selected book is us)
+ TbSync.providers.dav.overlayManager.injectOverlay(window, "chrome://dav4tbsync/content/overlays/addressbookdetailsoverlay.xhtml");
+ }
+}
diff --git a/content/overlays/addressbookoverlay.xhtml b/content/overlays/addressbookoverlay.xhtml
new file mode 100644
index 0000000..2917e43
--- /dev/null
+++ b/content/overlays/addressbookoverlay.xhtml
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+
+<overlay
+ id="TbSyncAbDavOverlay"
+ omscope="tbSyncDavAddressBook"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script type="application/javascript" src="chrome://dav4tbsync/content/overlays/addressbookoverlay.js" />
+
+</overlay>
diff --git a/content/provider.js b/content/provider.js
new file mode 100644
index 0000000..ad08f88
--- /dev/null
+++ b/content/provider.js
@@ -0,0 +1,785 @@
+/*
+ * 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";
+// check if getItem returns an array because of recursions!
+
+// Every object in here will be loaded into TbSync.providers.<providername>.
+const dav = TbSync.providers.dav;
+
+/**
+ * Implementing the TbSync interface for external provider extensions.
+ */
+
+var Base = class {
+ /**
+ * Called during load of external provider extension to init provider.
+ */
+ static async load() {
+ // Set default prefs
+ let branch = Services.prefs.getDefaultBranch("extensions.dav4tbsync.");
+ branch.setIntPref("maxitems", 50);
+ branch.setIntPref("timeout", 90000);
+ branch.setCharPref("clientID.type", "TbSync");
+ branch.setCharPref("clientID.type", "TbSync");
+ branch.setBoolPref("googlesupport", false);
+ branch.setBoolPref("enforceUniqueCalendarUrls", false);
+ branch.setCharPref("OAuth2_ClientID", "689460414096-e4nddn8tss5c59glidp4bc0qpeu3oper.apps.googleusercontent.com");
+ branch.setCharPref("OAuth2_ClientSecret", "LeTdF3UEpCvP1V3EBygjP-kl");
+
+ dav.openWindows = {};
+
+ let providerData = new TbSync.ProviderData("dav");
+ dav.overlayManager = new OverlayManager(providerData.extension, {verbose: 0});
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/abNewCardDialog.xhtml", "chrome://dav4tbsync/content/overlays/abNewCardWindow.xhtml");
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/abNewCardDialog.xhtml", "chrome://dav4tbsync/content/overlays/abCardWindow.xhtml");
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/abEditCardDialog.xhtml", "chrome://dav4tbsync/content/overlays/abCardWindow.xhtml");
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/addressbook.xhtml", "chrome://dav4tbsync/content/overlays/addressbookoverlay.xhtml");
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/addressbook.xhtml", "chrome://dav4tbsync/content/overlays/addressbookdetailsoverlay.xhtml");
+
+ // The abCSS.xul overlay is just adding a CSS file.
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/messengercompose/messengercompose.xhtml", "chrome://dav4tbsync/content/overlays/abCSS.xhtml");
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/abNewCardDialog.xhtml", "chrome://dav4tbsync/content/overlays/abCSS.xhtml");
+ await dav.overlayManager.registerOverlay("chrome://messenger/content/addressbook/addressbook.xhtml", "chrome://dav4tbsync/content/overlays/abCSS.xhtml");
+
+ dav.overlayManager.startObserving();
+ }
+
+
+ /**
+ * Called during unload of external provider extension to unload provider.
+ */
+ static async unload() {
+ dav.overlayManager.stopObserving();
+
+ // Close all open windows of this provider.
+ for (let id in dav.openWindows) {
+ if (dav.openWindows.hasOwnProperty(id)) {
+ try {
+ dav.openWindows[id].close();
+ } catch (e) {
+ //NOOP
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns string for the name of provider for the add account menu.
+ */
+ static getProviderName() {
+ return TbSync.getString("menu.name", "dav");
+ }
+
+
+ /**
+ * Returns version of the TbSync API this provider is using
+ */
+ static getApiVersion() { return "2.4"; }
+
+
+
+ /**
+ * Returns location of a provider icon.
+ */
+ static getProviderIcon(size, accountData = null) {
+ let root = "sabredav";
+ if (accountData) {
+ let serviceprovider = accountData.getAccountProperty("serviceprovider");
+ if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider)) {
+ root = dav.sync.serviceproviders[serviceprovider].icon;
+ }
+ }
+
+ switch (size) {
+ case 16:
+ return "chrome://dav4tbsync/content/skin/"+root+"16.png";
+ case 32:
+ return "chrome://dav4tbsync/content/skin/"+root+"32.png";
+ default :
+ return "chrome://dav4tbsync/content/skin/"+root+"48.png";
+ }
+ }
+
+
+ /**
+ * Returns a list of sponsors, they will be sorted by the index
+ */
+ static getSponsors() {
+ return {
+ "Thoben, Marc" : {name: "Marc Thoben", description: "Zimbra", icon: "", link: "" },
+ "Biebl, Michael" : {name: "Michael Biebl", description: "Nextcloud", icon: "", link: "" },
+ "László, Kovács" : {name: "Kovács László", description : "Radicale", icon: "", link: "" },
+ "Lütticke, David" : {name: "David Lütticke", description : "", icon: "", link: "" },
+ };
+ }
+
+
+ /**
+ * Returns the url of a page with details about contributors (used in the manager UI)
+ */
+ static getContributorsUrl() {
+ return "https://github.com/jobisoft/DAV-4-TbSync/blob/master/CONTRIBUTORS.md";
+ }
+
+
+ /**
+ * Returns the email address of the maintainer (used for bug reports).
+ */
+ static getMaintainerEmail() {
+ return "john.bieling@gmx.de";
+ }
+
+
+ /**
+ * Returns URL of the new account window.
+ *
+ * The URL will be opened via openDialog(), when the user wants to create a
+ * new account of this provider.
+ */
+ static getCreateAccountWindowUrl() {
+ return "chrome://dav4tbsync/content/manager/createAccount.xhtml";
+ }
+
+
+ /**
+ * Returns overlay XUL URL of the edit account dialog
+ * (chrome://tbsync/content/manager/editAccount.xhtml)
+ */
+ static getEditAccountOverlayUrl() {
+ return "chrome://dav4tbsync/content/manager/editAccountOverlay.xhtml";
+ }
+
+
+ /**
+ * Return object which contains all possible fields of a row in the
+ * accounts database with the default value if not yet stored in the
+ * database.
+ */
+ static getDefaultAccountEntries() {
+ let row = {
+ "useCalendarCache" : true,
+ "calDavHost" : "",
+ "cardDavHost" : "",
+ // these must return null if not defined
+ "calDavPrincipal" : null,
+ "cardDavPrincipal" : null,
+
+ "calDavOptions" : [],
+ "cardDavOptions" : [],
+
+ "serviceprovider" : "",
+ "serviceproviderRevision" : 0,
+
+ "user" : "",
+ "https" : true, //deprecated, because this is part of the URL now
+ "createdWithProviderVersion" : "0",
+ "syncGroups" : false,
+ };
+ return row;
+ }
+
+
+ /**
+ * Return object which contains all possible fields of a row in the folder
+ * database with the default value if not yet stored in the database.
+ */
+ static getDefaultFolderEntries() {
+ let folder = {
+ // different folders (caldav/carddav) can be stored on different
+ // servers (as with yahoo, icloud, gmx, ...), so we need to store
+ // the fqdn information per folders
+ "href" : "",
+ "https" : true,
+ "fqdn" : "",
+
+ "url" : "", // used by calendar to store the full url of this cal
+
+ "type" : "", //caldav, carddav or ics
+ "shared": false, //identify shared resources
+ "acl": "", //acl send from server
+ "target" : "",
+ "targetColor" : "",
+ "targetName" : "",
+ "ctag" : "",
+ "token" : "",
+ "createdWithProviderVersion" : "0",
+ };
+ return folder;
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is enabled in the
+ * manager UI.
+ */
+ static onEnableAccount(accountData) {
+ accountData.resetAccountProperty("calDavPrincipal");
+ accountData.resetAccountProperty("cardDavPrincipal");
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is disabled in the
+ * manager UI.
+ */
+ static onDisableAccount(accountData) {
+ }
+
+
+ /**
+ * Is called everytime an account of this provider is deleted in the
+ * manager UI.
+ */
+ static onDeleteAccount(accountData) {
+ dav.network.getAuthData(accountData).removeLoginData();
+ }
+
+
+ /**
+ * Implement this method, if this provider should add additional entries
+ * to the autocomplete list while typing something into the address field
+ * of the message composer.
+ */
+ static async abAutoComplete(accountData, currentQuery) {
+ /**
+ * Encode the string passed as value into an addressbook search term.
+ * The '(' and ')' characters are special for the addressbook
+ * search query language, but are not escaped in encodeURIComponent()
+ * so must be done manually on top of it.
+ */
+ function encodeABTermValue(aString) {
+ return encodeURIComponent(aString)
+ .replace(/\(/g, "%28")
+ .replace(/\)/g, "%29");
+ }
+
+ let modelQuery = "";
+ for (let attr of ["NickName", "FirstName", "LastName", "DisplayName", "PrimaryEmail", "SecondEmail", "X-DAV-JSON-Emails","ListName"]) {
+ modelQuery += "("+attr+",c,@V)"
+ }
+ modelQuery = "(or"+modelQuery+")";
+
+ // Instead of using accountData.getAllFolders() to get all folders of this account
+ // and then request and check the targets of each, we simply run over all address
+ // books and check for the directory property "tbSyncAccountID".
+ let entries = [];
+ let allAddressBooks = MailServices.ab.directories;
+ let fullString = currentQuery && currentQuery.trim().toLocaleLowerCase();
+
+ // If the search string is empty, or contains a comma, then just return
+ // no matches
+ // The comma check is so that we don't autocomplete against the user
+ // entering multiple addresses.
+ if (!fullString || fullString.includes(",")) {
+ return entries;
+ }
+
+ // Array of all the terms from the fullString search query
+ // check
+ // https://searchfox.org/comm-central/source/mailnews/addrbook/src/AbAutoCompleteSearch.jsm#460
+ // for quoted terms in search
+ let searchWords = fullString.split(" ");
+ let searchQuery = "";
+ searchWords.forEach(
+ searchWord => (searchQuery += modelQuery.replace(/@V/g, encodeABTermValue(searchWord)))
+ );
+
+ // searchQuery has all the (or(...)) searches, link them up with (and(...)).
+ searchQuery = "(and" + searchQuery + ")";
+
+ while (allAddressBooks.hasMoreElements()) {
+ let abook = allAddressBooks.getNext().QueryInterface(Components.interfaces.nsIAbDirectory);
+ if (abook instanceof Components.interfaces.nsIAbDirectory) { // or nsIAbItem or nsIAbCollection
+ if (TbSync.addressbook.getStringValue(abook, "tbSyncAccountID","") == accountData.accountID) {
+ let cards = await TbSync.addressbook.searchDirectory(abook.URI, searchQuery)
+ for (let card of cards) {
+ if (card.isMailList) {
+
+ entries.push({
+ value: card.getProperty("DisplayName", "") + " <"+ card.getProperty("DisplayName", "") +">",
+ comment: "",
+ icon: dav.Base.getProviderIcon(16, accountData),
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1653213
+ style: "dav4tbsync-abook",
+ });
+
+ } else {
+
+ let emailData = [];
+ try {
+ emailData = JSON.parse(card.getProperty("X-DAV-JSON-Emails","[]").trim());
+ } catch (e) {
+ //Components.utils.reportError(e);
+ }
+ for (let i = 0; i < emailData.length; i++) {
+ entries.push({
+ value: card.getProperty("DisplayName", [card.getProperty("FirstName",""), card.getProperty("LastName","")].join(" ")) + " <"+emailData[i].value+">",
+ comment: emailData[i].meta
+ .filter(entry => ["PREF","HOME","WORK"].includes(entry))
+ .map(entry => entry.toUpperCase() != "PREF" ? entry.toUpperCase() : entry.toLowerCase()).sort()
+ .map(entry => TbSync.getString("autocomplete." + entry.toUpperCase() , "dav"))
+ .join(", "),
+ icon: dav.Base.getProviderIcon(16, accountData),
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1653213
+ style: "dav4tbsync-abook",
+ });
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+ return entries;
+ }
+
+
+ /**
+ * Returns all folders of the account, sorted in the desired order.
+ * The most simple implementation is to return accountData.getAllFolders();
+ */
+ static getSortedFolders(accountData) {
+ let folders = accountData.getAllFolders();
+
+ // we can only sort arrays, so we create an array of objects which must
+ // contain the sort key and the associated folder
+ let toBeSorted = [];
+ for (let folder of folders) {
+ let t = 100;
+ switch (folder.getFolderProperty("type")) {
+ case "carddav":
+ t+=0;
+ break;
+ case "caldav":
+ t+=1;
+ break;
+ case "ics":
+ t+=2;
+ break;
+ default:
+ t+=9;
+ break;
+ }
+
+ if (folder.getFolderProperty("shared")) {
+ t+=100;
+ }
+
+ toBeSorted.push({"key": t.toString() + folder.getFolderProperty("foldername"), "folder": folder});
+ }
+
+ //sort
+ toBeSorted.sort(function(a,b) {
+ return a.key > b.key;
+ });
+
+ let sortedFolders = [];
+ for (let sortObj of toBeSorted) {
+ sortedFolders.push(sortObj.folder);
+ }
+ return sortedFolders;
+ }
+
+
+ /**
+ * Return the connection timeout for an active sync, so TbSync can append
+ * a countdown to the connection timeout, while waiting for an answer from
+ * the server. Only syncstates which start with "send." will trigger this.
+ */
+ static getConnectionTimeout(accountData) {
+ return dav.sync.prefSettings.getIntPref("timeout");
+ }
+
+
+ /**
+ * Is called if TbSync needs to synchronize the folder list.
+ */
+ static async syncFolderList(syncData, syncJob, syncRunNr) {
+ // Recommendation: Put the actual function call inside a try catch, to
+ // ensure returning a proper StatusData object, regardless of what
+ // happens inside that function. You may also throw custom errors
+ // in that function, which have the StatusData obj attached, which
+ // should be returned.
+
+ try {
+ await dav.sync.folderList(syncData);
+ } catch (e) {
+ if (e.name == "dav4tbsync") {
+ return e.statusData;
+ } else {
+ Components.utils.reportError(e);
+ // re-throw any other error and let TbSync handle it
+ throw (e);
+ }
+ }
+
+ // we fall through, if there was no error
+ return new TbSync.StatusData();
+ }
+
+
+ /**
+ * Is called if TbSync needs to synchronize a folder.
+ */
+ static async syncFolder(syncData, syncJob, syncRunNr) {
+ // Recommendation: Put the actual function call inside a try catch, to
+ // ensure returning a proper StatusData object, regardless of what
+ // happens inside that function. You may also throw custom errors
+ // in that function, which have the StatusData obj attached, which
+ // should be returned.
+
+ // Limit auto sync rate, if google
+ let isGoogle = (syncData.accountData.getAccountProperty("serviceprovider") == "google");
+ let isDefaultGoogleApp = (Services.prefs.getDefaultBranch("extensions.dav4tbsync.").getCharPref("OAuth2_ClientID") == dav.sync.prefSettings.getCharPref("OAuth2_ClientID"));
+ if (isGoogle && isDefaultGoogleApp && syncData.accountData.getAccountProperty("autosync") > 0 && syncData.accountData.getAccountProperty("autosync") < 30) {
+ syncData.accountData.setAccountProperty("autosync", 30);
+ TbSync.eventlog.add("warning", syncData.eventLogInfo, "Lowering sync interval to 30 minutes to reduce google request rate on standard TbSync Google APP (limited to 2.000.000 requests per day).");
+ }
+
+ // Process a single folder.
+ try {
+ await dav.sync.folder(syncData);
+ } catch (e) {
+ if (e.name == "dav4tbsync") {
+ return e.statusData;
+ } else {
+ Components.utils.reportError(e);
+ // re-throw any other error and let TbSync handle it
+ throw (e);
+ }
+ }
+
+ // we fall through, if there was no error
+ return new TbSync.StatusData();
+ }
+}
+
+
+
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// * TargetData implementation
+// * Using TbSyncs advanced address book TargetData
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+var TargetData_addressbook = class extends TbSync.addressbook.AdvancedTargetData {
+ constructor(folderData) {
+ super(folderData);
+ }
+
+ // define a card property, which should be used for the changelog
+ // basically your primary key for the abItem properties
+ // UID will be used, if nothing specified
+ get primaryKeyField() {
+ return "X-DAV-HREF"
+ }
+
+ generatePrimaryKey() {
+ return this.folderData.getFolderProperty("href") + TbSync.generateUUID() + ".vcf";
+ }
+
+ // enable or disable changelog
+ get logUserChanges() {
+ return true;
+ }
+
+ directoryObserver(aTopic) {
+ switch (aTopic) {
+ case "addrbook-removed":
+ case "addrbook-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] " + this.folderData.getFolderProperty("foldername"));
+ break;
+ }
+ }
+
+ cardObserver(aTopic, abCardItem) {
+ switch (aTopic) {
+ case "addrbook-contact-updated":
+ case "addrbook-contact-removed":
+ //Services.console.logStringMessage("["+ aTopic + "] " + abCardItem.getProperty("DisplayName"));
+ break;
+
+ case "addrbook-contact-created":
+ {
+ //Services.console.logStringMessage("["+ aTopic + "] Created new X-DAV-UID for Card <"+ abCardItem.getProperty("DisplayName")+">");
+ abCardItem.setProperty("X-DAV-UID", TbSync.generateUUID());
+ // the card is tagged with "_by_user" so it will not be changed to "_by_server" by the following modify
+ abCardItem.abDirectory.modifyItem(abCardItem);
+ break;
+ }
+ }
+ dav.sync.onChange(abCardItem);
+ }
+
+ listObserver(aTopic, abListItem, abListMember) {
+ switch (aTopic) {
+ case "addrbook-list-member-added":
+ case "addrbook-list-member-removed":
+ //Services.console.logStringMessage("["+ aTopic + "] MemberName: " + abListMember.getProperty("DisplayName"));
+ break;
+
+ case "addrbook-list-removed":
+ case "addrbook-list-updated":
+ //Services.console.logStringMessage("["+ aTopic + "] ListName: " + abListItem.getProperty("ListName"));
+ break;
+
+ case "addrbook-list-created":
+ //Services.console.logStringMessage("["+ aTopic + "] Created new X-DAV-UID for List <"+abListItem.getProperty("ListName")+">");
+ abListItem.setProperty("X-DAV-UID", TbSync.generateUUID());
+ // custom props of lists get updated directly, no need to call .modify()
+ break;
+ }
+ dav.sync.onChange(abListItem);
+ }
+
+ async createAddressbook(newname) {
+ // https://searchfox.org/comm-central/source/mailnews/addrbook/src/nsDirPrefs.h
+ let dirPrefId = MailServices.ab.newAddressBook(newname, "", 101);
+ let directory = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ dav.sync.resetFolderSyncInfo(this.folderData);
+
+ if (directory && directory instanceof Components.interfaces.nsIAbDirectory && directory.dirPrefId == dirPrefId) {
+ let serviceprovider = this.folderData.accountData.getAccountProperty("serviceprovider");
+ let icon = "custom";
+ if (dav.sync.serviceproviders.hasOwnProperty(serviceprovider)) {
+ icon = dav.sync.serviceproviders[serviceprovider].icon;
+ }
+ directory.setStringValue("tbSyncIcon", "dav" + icon);
+
+ // Disable AutoComplete, so we can have full control over the auto completion of our own directories.
+ // Implemented by me in https://bugzilla.mozilla.org/show_bug.cgi?id=1546425
+ directory.setBoolValue("enable_autocomplete", false);
+
+ return directory;
+ }
+ return null;
+ }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+// * TargetData implementation
+// * Using TbSyncs advanced calendar TargetData
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+
+var TargetData_calendar = class extends TbSync.lightning.AdvancedTargetData {
+ constructor(folderData) {
+ super(folderData);
+ }
+ // The calendar target does not support a custom primaryKeyField, because
+ // the lightning implementation only allows to search for items via UID.
+ // Like the addressbook target, the calendar target item element has a
+ // primaryKey getter/setter which - however - only works on the UID.
+
+ // enable or disable changelog
+ get logUserChanges(){
+ return false;
+ }
+
+ calendarObserver(aTopic, tbCalendar, aPropertyName, aPropertyValue, aOldPropertyValue) {
+ switch (aTopic) {
+ case "onCalendarPropertyChanged":
+ {
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbCalendar.calendar.name + " : " + aPropertyName);
+ switch (aPropertyName) {
+ case "color":
+ if (aOldPropertyValue.toString().toUpperCase() != aPropertyValue.toString().toUpperCase()) {
+ //prepare connection data
+ let connection = new dav.network.ConnectionData(this.folderData);
+ //update color on server
+ dav.network.sendRequest("<d:propertyupdate "+dav.tools.xmlns(["d","apple"])+"><d:set><d:prop><apple:calendar-color>"+(aPropertyValue + "FFFFFFFF").slice(0,9)+"</apple:calendar-color></d:prop></d:set></d:propertyupdate>", this.folderData.getFolderProperty("href"), "PROPPATCH", connection);
+ }
+ break;
+ }
+ }
+ break;
+
+ case "onCalendarDeleted":
+ case "onCalendarPropertyDeleted":
+ //Services.console.logStringMessage("["+ aTopic + "] " +tbCalendar.calendar.name);
+ break;
+ }
+ }
+
+ itemObserver(aTopic, tbItem, tbOldItem) {
+ switch (aTopic) {
+ case "onAddItem":
+ case "onModifyItem":
+ case "onDeleteItem":
+ //Services.console.logStringMessage("["+ aTopic + "] " + tbItem.nativeItem.title);
+ break;
+ }
+ }
+
+ async createCalendar(newname) {
+ let calManager = TbSync.lightning.cal.getCalendarManager();
+ let authData = dav.network.getAuthData(this.folderData.accountData);
+
+ let caltype = this.folderData.getFolderProperty("type");
+ let isGoogle = (this.folderData.accountData.getAccountProperty("serviceprovider") == "google");
+
+ let baseUrl = "";
+ if (isGoogle) {
+ baseUrl = "http" + (this.folderData.getFolderProperty("https") ? "s" : "") + "://" + this.folderData.accountID + "@" + this.folderData.getFolderProperty("fqdn");
+ } else if (caltype == "caldav") {
+ baseUrl = "http" + (this.folderData.getFolderProperty("https") ? "s" : "") + "://" + this.folderData.getFolderProperty("fqdn");
+ }
+
+ let url = dav.tools.parseUri(baseUrl + this.folderData.getFolderProperty("href") + (dav.sync.prefSettings.getBoolPref("enforceUniqueCalendarUrls") ? "?" + this.folderData.accountID : ""));
+ this.folderData.setFolderProperty("url", url.spec);
+
+ //check if that calendar already exists
+ let cals = calManager.getCalendars({});
+ let newCalendar = null;
+ let found = false;
+ for (let calendar of calManager.getCalendars({})) {
+ if (calendar.uri.spec == url.spec) {
+ newCalendar = calendar;
+ found = true;
+ break;
+ }
+ }
+
+
+ if (found) {
+ newCalendar.setProperty("username", authData.username);
+ newCalendar.setProperty("color", this.folderData.getFolderProperty("targetColor"));
+ newCalendar.name = newname;
+ } else {
+ newCalendar = calManager.createCalendar((isGoogle ? "tbSyncCalDav" : caltype), url); //tbSyncCalDav, caldav or ics
+ newCalendar.id = TbSync.lightning.cal.getUUID();
+ newCalendar.name = newname;
+
+ newCalendar.setProperty("username", authData.username);
+ newCalendar.setProperty("color", this.folderData.getFolderProperty("targetColor"));
+ // removed in TB78, as it seems to not fully enable the calendar, if present before registering
+ // https://searchfox.org/comm-central/source/calendar/base/content/calendar-management.js#385
+ //newCalendar.setProperty("calendar-main-in-composite",true);
+ newCalendar.setProperty("cache.enabled", this.folderData.accountData.getAccountProperty("useCalendarCache"));
+ }
+
+ if (this.folderData.getFolderProperty("downloadonly")) newCalendar.setProperty("readOnly", true);
+
+ // Setup password for Lightning calendar, so users do not get prompted (ICS and google urls do not need a password)
+ if (caltype == "caldav" && !isGoogle) {
+ TbSync.dump("Searching CalDAV authRealm for", url.host);
+ let connectionData = new dav.network.ConnectionData(this.folderData);
+ let response = await dav.network.sendRequest("<d:propfind "+dav.tools.xmlns(["d"])+"><d:prop><d:resourcetype /><d:displayname /></d:prop></d:propfind>", url.spec , "PROPFIND", connectionData, {"Depth": "0", "Prefer": "return=minimal"}, {containerRealm: "setup", containerReset: true, passwordRetries: 0});
+
+ let realm = connectionData.realm || "";
+ if (realm !== "") {
+ TbSync.dump("Adding Lightning password", "User <"+authData.username+">, Realm <"+realm+">");
+ //manually create a lightning style entry in the password manager
+ TbSync.passwordManager.updateLoginInfo(url.prePath, realm, /* old */ authData.username, /* new */ authData.username, authData.password);
+ }
+ }
+
+ if (!found) {
+ calManager.registerCalendar(newCalendar);
+ }
+ return newCalendar;
+ }
+}
+
+
+
+
+
+/**
+ * This provider is implementing the StandardFolderList class instead of
+ * the FolderList class.
+ */
+var StandardFolderList = class {
+ /**
+ * Is called before the context menu of the folderlist is shown, allows to
+ * show/hide custom menu options based on selected folder. During an active
+ * sync, folderData will be null.
+ */
+ static onContextMenuShowing(window, folderData) {
+ }
+
+
+ /**
+ * Return the icon used in the folderlist to represent the different folder
+ * types.
+ */
+ static getTypeImage(folderData) {
+ let src = "";
+ switch (folderData.getFolderProperty("type")) {
+ case "carddav":
+ if (folderData.getFolderProperty("shared")) {
+ return "chrome://tbsync/content/skin/contacts16_shared.png";
+ } else {
+ return "chrome://tbsync/content/skin/contacts16.png";
+ }
+ case "caldav":
+ if (folderData.getFolderProperty("shared")) {
+ return "chrome://tbsync/content/skin/calendar16_shared.png";
+ } else {
+ return "chrome://tbsync/content/skin/calendar16.png";
+ }
+ case "ics":
+ return "chrome://dav4tbsync/content/skin/ics16.png";
+ }
+ }
+
+
+ /**
+ * Return the name of the folder shown in the folderlist.
+ */
+ static getFolderDisplayName(folderData) {
+ return folderData.getFolderProperty("foldername");
+ }
+
+
+ /**
+ * Return the attributes for the ACL RO (readonly) menu element per folder.
+ * (label, disabled, hidden, style, ...)
+ *
+ * Return a list of attributes and their values. If both (RO+RW) do
+ * not return any attributes, the ACL menu is not displayed at all.
+ */
+ static getAttributesRoAcl(folderData) {
+ return {
+ label: TbSync.getString("acl.readonly", "dav"),
+ };
+ }
+
+
+ /**
+ * Return the attributes for the ACL RW (readwrite) menu element per folder.
+ * (label, disabled, hidden, style, ...)
+ *
+ * Return a list of attributes and their values. If both (RO+RW) do
+ * not return any attributes, the ACL menu is not displayed at all.
+ */
+ static getAttributesRwAcl(folderData) {
+ let acl = parseInt(folderData.getFolderProperty("acl"));
+ let acls = [];
+ if (acl & 0x2) acls.push(TbSync.getString("acl.modify", "dav"));
+ if (acl & 0x4) acls.push(TbSync.getString("acl.add", "dav"));
+ if (acl & 0x8) acls.push(TbSync.getString("acl.delete", "dav"));
+ if (acls.length == 0) acls.push(TbSync.getString("acl.none", "dav"));
+
+ return {
+ label: TbSync.getString("acl.readwrite::"+acls.join(", "), "dav"),
+ disabled: (acl & 0x7) != 0x7,
+ }
+ }
+}
+
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/sync.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/abUI.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/tools.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/network.js", this, "UTF-8");
+Services.scriptloader.loadSubScript("chrome://dav4tbsync/content/includes/vcard/vcard.js", this, "UTF-8");
diff --git a/content/skin/ab.css b/content/skin/ab.css
new file mode 100644
index 0000000..a1398da
--- /dev/null
+++ b/content/skin/ab.css
@@ -0,0 +1,82 @@
+treechildren::-moz-tree-image(DirCol, davgoogle) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/google16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davweb) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/web16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davfruux) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/fruux16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davposteo) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/posteo16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davmbo) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/mbo16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davicloud) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/icloud16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davyahoo) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/yahoo16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davgmx) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/gmx16.png");
+}
+
+treechildren::-moz-tree-image(DirCol, davcustom) {
+ margin-inline-end: 2px;
+ list-style-image: url("chrome://dav4tbsync/content/skin/sabredav16.png");
+}
+
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davgoogle"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/google16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davweb"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/web16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davfruux"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/fruux16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davposteo"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/posteo16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davmbo"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/mbo16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davicloud"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/icloud16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davyahoo"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/yahoo16.png");
+}
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davgmx"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/gmx16.png");
+}
+
+
+.abMenuItem[AddrBook="true"][TbSyncIcon="davcustom"] {
+ list-style-image: url("chrome://dav4tbsync/content/skin/sabredav16.png");
+}
diff --git a/content/skin/arrow.down10.png b/content/skin/arrow.down10.png
new file mode 100644
index 0000000..82eccb5
--- /dev/null
+++ b/content/skin/arrow.down10.png
Binary files differ
diff --git a/content/skin/arrow.up10.png b/content/skin/arrow.up10.png
new file mode 100644
index 0000000..db87a3d
--- /dev/null
+++ b/content/skin/arrow.up10.png
Binary files differ
diff --git a/content/skin/dragdrop.png b/content/skin/dragdrop.png
new file mode 100644
index 0000000..a3797da
--- /dev/null
+++ b/content/skin/dragdrop.png
Binary files differ
diff --git a/content/skin/fruux16.png b/content/skin/fruux16.png
new file mode 100644
index 0000000..61d25f2
--- /dev/null
+++ b/content/skin/fruux16.png
Binary files differ
diff --git a/content/skin/fruux32.png b/content/skin/fruux32.png
new file mode 100644
index 0000000..2d37e6b
--- /dev/null
+++ b/content/skin/fruux32.png
Binary files differ
diff --git a/content/skin/fruux48.png b/content/skin/fruux48.png
new file mode 100644
index 0000000..c2b9aca
--- /dev/null
+++ b/content/skin/fruux48.png
Binary files differ
diff --git a/content/skin/gmx16.png b/content/skin/gmx16.png
new file mode 100644
index 0000000..1672e4a
--- /dev/null
+++ b/content/skin/gmx16.png
Binary files differ
diff --git a/content/skin/gmx32.png b/content/skin/gmx32.png
new file mode 100644
index 0000000..bb2dd43
--- /dev/null
+++ b/content/skin/gmx32.png
Binary files differ
diff --git a/content/skin/gmx48.png b/content/skin/gmx48.png
new file mode 100644
index 0000000..d23c125
--- /dev/null
+++ b/content/skin/gmx48.png
Binary files differ
diff --git a/content/skin/google16.png b/content/skin/google16.png
new file mode 100644
index 0000000..7847bfc
--- /dev/null
+++ b/content/skin/google16.png
Binary files differ
diff --git a/content/skin/google32.png b/content/skin/google32.png
new file mode 100644
index 0000000..b30b81c
--- /dev/null
+++ b/content/skin/google32.png
Binary files differ
diff --git a/content/skin/google48.png b/content/skin/google48.png
new file mode 100644
index 0000000..23d0540
--- /dev/null
+++ b/content/skin/google48.png
Binary files differ
diff --git a/content/skin/icloud16.png b/content/skin/icloud16.png
new file mode 100644
index 0000000..4399f6a
--- /dev/null
+++ b/content/skin/icloud16.png
Binary files differ
diff --git a/content/skin/icloud32.png b/content/skin/icloud32.png
new file mode 100644
index 0000000..6691ebc
--- /dev/null
+++ b/content/skin/icloud32.png
Binary files differ
diff --git a/content/skin/icloud48.png b/content/skin/icloud48.png
new file mode 100644
index 0000000..2171c1d
--- /dev/null
+++ b/content/skin/icloud48.png
Binary files differ
diff --git a/content/skin/ics16.png b/content/skin/ics16.png
new file mode 100644
index 0000000..7fc8ab6
--- /dev/null
+++ b/content/skin/ics16.png
Binary files differ
diff --git a/content/skin/mbo16.png b/content/skin/mbo16.png
new file mode 100644
index 0000000..bbf127f
--- /dev/null
+++ b/content/skin/mbo16.png
Binary files differ
diff --git a/content/skin/mbo32.png b/content/skin/mbo32.png
new file mode 100644
index 0000000..ee86331
--- /dev/null
+++ b/content/skin/mbo32.png
Binary files differ
diff --git a/content/skin/mbo48.png b/content/skin/mbo48.png
new file mode 100644
index 0000000..90c62dc
--- /dev/null
+++ b/content/skin/mbo48.png
Binary files differ
diff --git a/content/skin/posteo16.png b/content/skin/posteo16.png
new file mode 100644
index 0000000..922078b
--- /dev/null
+++ b/content/skin/posteo16.png
Binary files differ
diff --git a/content/skin/posteo32.png b/content/skin/posteo32.png
new file mode 100644
index 0000000..b296393
--- /dev/null
+++ b/content/skin/posteo32.png
Binary files differ
diff --git a/content/skin/posteo48.png b/content/skin/posteo48.png
new file mode 100644
index 0000000..a1f015d
--- /dev/null
+++ b/content/skin/posteo48.png
Binary files differ
diff --git a/content/skin/sabredav16.png b/content/skin/sabredav16.png
new file mode 100644
index 0000000..fda1f68
--- /dev/null
+++ b/content/skin/sabredav16.png
Binary files differ
diff --git a/content/skin/sabredav32.png b/content/skin/sabredav32.png
new file mode 100644
index 0000000..b0e80d6
--- /dev/null
+++ b/content/skin/sabredav32.png
Binary files differ
diff --git a/content/skin/sabredav48.png b/content/skin/sabredav48.png
new file mode 100644
index 0000000..48a9739
--- /dev/null
+++ b/content/skin/sabredav48.png
Binary files differ
diff --git a/content/skin/type.car10.png b/content/skin/type.car10.png
new file mode 100644
index 0000000..ca12b71
--- /dev/null
+++ b/content/skin/type.car10.png
Binary files differ
diff --git a/content/skin/type.car16.png b/content/skin/type.car16.png
new file mode 100644
index 0000000..db6fb9d
--- /dev/null
+++ b/content/skin/type.car16.png
Binary files differ
diff --git a/content/skin/type.cell10.png b/content/skin/type.cell10.png
new file mode 100644
index 0000000..ef5ebac
--- /dev/null
+++ b/content/skin/type.cell10.png
Binary files differ
diff --git a/content/skin/type.cell16.png b/content/skin/type.cell16.png
new file mode 100644
index 0000000..0716347
--- /dev/null
+++ b/content/skin/type.cell16.png
Binary files differ
diff --git a/content/skin/type.fax10.png b/content/skin/type.fax10.png
new file mode 100644
index 0000000..276f0d1
--- /dev/null
+++ b/content/skin/type.fax10.png
Binary files differ
diff --git a/content/skin/type.fax16.png b/content/skin/type.fax16.png
new file mode 100644
index 0000000..4b9a92c
--- /dev/null
+++ b/content/skin/type.fax16.png
Binary files differ
diff --git a/content/skin/type.home10.png b/content/skin/type.home10.png
new file mode 100644
index 0000000..8a72f0f
--- /dev/null
+++ b/content/skin/type.home10.png
Binary files differ
diff --git a/content/skin/type.home16.png b/content/skin/type.home16.png
new file mode 100644
index 0000000..84d829b
--- /dev/null
+++ b/content/skin/type.home16.png
Binary files differ
diff --git a/content/skin/type.nopref.png b/content/skin/type.nopref.png
new file mode 100644
index 0000000..27caa40
--- /dev/null
+++ b/content/skin/type.nopref.png
Binary files differ
diff --git a/content/skin/type.other10.png b/content/skin/type.other10.png
new file mode 100644
index 0000000..d05fc5f
--- /dev/null
+++ b/content/skin/type.other10.png
Binary files differ
diff --git a/content/skin/type.other16.png b/content/skin/type.other16.png
new file mode 100644
index 0000000..60a169b
--- /dev/null
+++ b/content/skin/type.other16.png
Binary files differ
diff --git a/content/skin/type.pager10.png b/content/skin/type.pager10.png
new file mode 100644
index 0000000..5c6fbe7
--- /dev/null
+++ b/content/skin/type.pager10.png
Binary files differ
diff --git a/content/skin/type.pager16.png b/content/skin/type.pager16.png
new file mode 100644
index 0000000..6a0f2b1
--- /dev/null
+++ b/content/skin/type.pager16.png
Binary files differ
diff --git a/content/skin/type.pref.png b/content/skin/type.pref.png
new file mode 100644
index 0000000..409a30c
--- /dev/null
+++ b/content/skin/type.pref.png
Binary files differ
diff --git a/content/skin/type.video10.png b/content/skin/type.video10.png
new file mode 100644
index 0000000..188cabc
--- /dev/null
+++ b/content/skin/type.video10.png
Binary files differ
diff --git a/content/skin/type.video16.png b/content/skin/type.video16.png
new file mode 100644
index 0000000..afb8c7c
--- /dev/null
+++ b/content/skin/type.video16.png
Binary files differ
diff --git a/content/skin/type.voice10.png b/content/skin/type.voice10.png
new file mode 100644
index 0000000..3ed48b6
--- /dev/null
+++ b/content/skin/type.voice10.png
Binary files differ
diff --git a/content/skin/type.voice16.png b/content/skin/type.voice16.png
new file mode 100644
index 0000000..debb016
--- /dev/null
+++ b/content/skin/type.voice16.png
Binary files differ
diff --git a/content/skin/type.work10.png b/content/skin/type.work10.png
new file mode 100644
index 0000000..10db4e7
--- /dev/null
+++ b/content/skin/type.work10.png
Binary files differ
diff --git a/content/skin/type.work16.png b/content/skin/type.work16.png
new file mode 100644
index 0000000..de3036f
--- /dev/null
+++ b/content/skin/type.work16.png
Binary files differ
diff --git a/content/skin/web16.png b/content/skin/web16.png
new file mode 100644
index 0000000..3a738a5
--- /dev/null
+++ b/content/skin/web16.png
Binary files differ
diff --git a/content/skin/web32.png b/content/skin/web32.png
new file mode 100644
index 0000000..234e2f8
--- /dev/null
+++ b/content/skin/web32.png
Binary files differ
diff --git a/content/skin/web48.png b/content/skin/web48.png
new file mode 100644
index 0000000..6909c9e
--- /dev/null
+++ b/content/skin/web48.png
Binary files differ
diff --git a/content/skin/yahoo16.png b/content/skin/yahoo16.png
new file mode 100644
index 0000000..7225a3a
--- /dev/null
+++ b/content/skin/yahoo16.png
Binary files differ
diff --git a/content/skin/yahoo32.png b/content/skin/yahoo32.png
new file mode 100644
index 0000000..054ec55
--- /dev/null
+++ b/content/skin/yahoo32.png
Binary files differ
diff --git a/content/skin/yahoo48.png b/content/skin/yahoo48.png
new file mode 100644
index 0000000..bfd7d31
--- /dev/null
+++ b/content/skin/yahoo48.png
Binary files differ
diff --git a/crowdin.yml b/crowdin.yml
new file mode 100644
index 0000000..c3c6508
--- /dev/null
+++ b/crowdin.yml
@@ -0,0 +1,5 @@
+commit_message: https://crowdin.com/project/dav-4-tbsync
+files:
+ - source: /_locales/en-US/*
+ translation: /_locales/%osx_locale%/%original_file_name%
+ escape_special_characters: 0
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..6e6ad79
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,32 @@
+{
+ "applications": {
+ "gecko": {
+ "id": "dav4tbsync@jobisoft.de",
+ "strict_min_version": "78.0",
+ "strict_max_version": "78.*"
+ }
+ },
+ "manifest_version": 2,
+ "name": "__MSG_extensionName__",
+ "version": "1.23",
+ "author": "John Bieling",
+ "homepage_url": "https://github.com/jobisoft/DAV-4-TbSync/",
+ "default_locale": "en-US",
+ "description": "__MSG_extensionDescription__",
+ "icons": {
+ "32": "content/skin/sabredav32.png"
+ },
+ "background": {
+ "scripts": ["background.js"]
+ },
+ "experiment_apis": {
+ "BootstrapLoader": {
+ "schema": "content/api/BootstrapLoader/schema.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "paths": [["BootstrapLoader"]],
+ "script": "content/api/BootstrapLoader/implementation.js"
+ }
+ }
+ }
+}
diff --git a/screenshots/1.png b/screenshots/1.png
new file mode 100644
index 0000000..58b98b2
--- /dev/null
+++ b/screenshots/1.png
Binary files differ
diff --git a/screenshots/2.png b/screenshots/2.png
new file mode 100644
index 0000000..81d1c04
--- /dev/null
+++ b/screenshots/2.png
Binary files differ
diff --git a/screenshots/3.png b/screenshots/3.png
new file mode 100644
index 0000000..9a4f1e9
--- /dev/null
+++ b/screenshots/3.png
Binary files differ
diff --git a/screenshots/4.png b/screenshots/4.png
new file mode 100644
index 0000000..63730a5
--- /dev/null
+++ b/screenshots/4.png
Binary files differ
diff --git a/screenshots/5.png b/screenshots/5.png
new file mode 100644
index 0000000..e5a32fb
--- /dev/null
+++ b/screenshots/5.png
Binary files differ
diff --git a/screenshots/6.png b/screenshots/6.png
new file mode 100644
index 0000000..e15f2a9
--- /dev/null
+++ b/screenshots/6.png
Binary files differ
diff --git a/screenshots/AddAccount.png b/screenshots/AddAccount.png
new file mode 100644
index 0000000..cb0c493
--- /dev/null
+++ b/screenshots/AddAccount.png
Binary files differ
diff --git a/screenshots/custom-service.PNG b/screenshots/custom-service.PNG
new file mode 100644
index 0000000..374f0cc
--- /dev/null
+++ b/screenshots/custom-service.PNG
Binary files differ
diff --git a/screenshots/discoveryservice.PNG b/screenshots/discoveryservice.PNG
new file mode 100644
index 0000000..e81e433
--- /dev/null
+++ b/screenshots/discoveryservice.PNG
Binary files differ
diff --git a/screenshots/icloud.png b/screenshots/icloud.png
new file mode 100644
index 0000000..8228d24
--- /dev/null
+++ b/screenshots/icloud.png
Binary files differ
diff --git a/screenshots/icloudlist.png b/screenshots/icloudlist.png
new file mode 100644
index 0000000..37d7310
--- /dev/null
+++ b/screenshots/icloudlist.png
Binary files differ
diff --git a/screenshots/install.png b/screenshots/install.png
new file mode 100644
index 0000000..6f5bfd7
--- /dev/null
+++ b/screenshots/install.png
Binary files differ
diff --git a/screenshots/options.png b/screenshots/options.png
new file mode 100644
index 0000000..3e9cab8
--- /dev/null
+++ b/screenshots/options.png
Binary files differ
diff --git a/screenshots/posteo.PNG b/screenshots/posteo.PNG
new file mode 100644
index 0000000..775d024
--- /dev/null
+++ b/screenshots/posteo.PNG
Binary files differ
diff --git a/screenshots/sercviceselection.png b/screenshots/sercviceselection.png
new file mode 100644
index 0000000..8bdb5e5
--- /dev/null
+++ b/screenshots/sercviceselection.png
Binary files differ
diff --git a/unused/_mbo16.png b/unused/_mbo16.png
new file mode 100644
index 0000000..a0ecec5
--- /dev/null
+++ b/unused/_mbo16.png
Binary files differ
diff --git a/unused/_mbo32.png b/unused/_mbo32.png
new file mode 100644
index 0000000..a08c4b4
--- /dev/null
+++ b/unused/_mbo32.png
Binary files differ
diff --git a/unused/_mbo48.png b/unused/_mbo48.png
new file mode 100644
index 0000000..128f729
--- /dev/null
+++ b/unused/_mbo48.png
Binary files differ
diff --git a/unused/google b/unused/google
new file mode 100644
index 0000000..b0a07fe
--- /dev/null
+++ b/unused/google
@@ -0,0 +1,13 @@
+My app uses https://www.googleapis.com/auth/carddav and https://www.googleapis.com/auth/calendar to allow Thunderbird users to access and manage their google contacts and calendars within the Thunderbird addressbook and the Thunderbird calendar.
+
+My app also allows to modify, add and delete contacts and events, I therefore need full write access.
+
+
+
+I have created a video, that shows my app in action and also shows some details of the network traffic generated by my app, where the client id and scopes transmitted to teh google server can be seen:
+
+https://tbsync.jobisoft.de/google/google_v2.mp4
+
+This is an updated verification request, as I did not include my scopes in the first request. I also changed the app icon to match the icon of the app as shown in Thunderbird:
+
+https://tbsync.jobisoft.de/google/ \ No newline at end of file
diff --git a/unused/iconfinder_17_939744_16.png b/unused/iconfinder_17_939744_16.png
new file mode 100644
index 0000000..00eab4b
--- /dev/null
+++ b/unused/iconfinder_17_939744_16.png
Binary files differ
diff --git a/unused/iconfinder_17_939744_32.png b/unused/iconfinder_17_939744_32.png
new file mode 100644
index 0000000..baacddc
--- /dev/null
+++ b/unused/iconfinder_17_939744_32.png
Binary files differ
diff --git a/unused/iconfinder_17_939744_64.png b/unused/iconfinder_17_939744_64.png
new file mode 100644
index 0000000..b6a227b
--- /dev/null
+++ b/unused/iconfinder_17_939744_64.png
Binary files differ
diff --git a/unused/iconfinder_Apple_1298725_16.png b/unused/iconfinder_Apple_1298725_16.png
new file mode 100644
index 0000000..fd5fa79
--- /dev/null
+++ b/unused/iconfinder_Apple_1298725_16.png
Binary files differ
diff --git a/unused/iconfinder_Apple_1298725_32.png b/unused/iconfinder_Apple_1298725_32.png
new file mode 100644
index 0000000..0815a20
--- /dev/null
+++ b/unused/iconfinder_Apple_1298725_32.png
Binary files differ
diff --git a/unused/iconfinder_Apple_1298725_64.png b/unused/iconfinder_Apple_1298725_64.png
new file mode 100644
index 0000000..2d05773
--- /dev/null
+++ b/unused/iconfinder_Apple_1298725_64.png
Binary files differ
diff --git a/unused/mbo48_2.png b/unused/mbo48_2.png
new file mode 100644
index 0000000..0379947
--- /dev/null
+++ b/unused/mbo48_2.png
Binary files differ