├── skin
├── icon16.png
├── icon32.png
└── icon64.png
├── api
├── Google4TbSync
│ ├── schema.json
│ └── implementation.js
└── LegacyHelper
│ ├── schema.json
│ ├── README.md
│ └── implementation.js
├── CONTRIBUTORS.md
├── background.js
├── content
├── locales.js
├── manager
│ ├── authenticate.xhtml
│ ├── editAccountOverlay.js
│ ├── createAccount.js
│ ├── createAccount.xhtml
│ └── editAccountOverlay.xhtml
├── includes
│ ├── NetworkError.js
│ ├── ResponseError.js
│ ├── IllegalArgumentError.js
│ ├── AuthorizationCodeError.js
│ ├── Logger.js
│ ├── sync.js
│ ├── PeopleAPI.js
│ └── AddressBookSynchronizer.js
└── provider.js
├── _locales
├── nb-NO
│ └── messages.json
├── nl
│ └── messages.json
├── de
│ └── messages.json
├── en-US
│ └── messages.json
└── it
│ └── messages.json
├── manifest.json
├── README.md
└── LICENSE
/skin/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zanonmark/Google-4-TbSync/HEAD/skin/icon16.png
--------------------------------------------------------------------------------
/skin/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zanonmark/Google-4-TbSync/HEAD/skin/icon32.png
--------------------------------------------------------------------------------
/skin/icon64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zanonmark/Google-4-TbSync/HEAD/skin/icon64.png
--------------------------------------------------------------------------------
/api/Google4TbSync/schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "namespace": "Google4TbSync",
4 | "functions": [
5 | {
6 | "name": "load",
7 | "type": "function",
8 | "async": true,
9 | "parameters": []
10 | }
11 | ]
12 | }
13 | ]
--------------------------------------------------------------------------------
/CONTRIBUTORS.md:
--------------------------------------------------------------------------------
1 | ## Development
2 | * Marco Zanon
3 |
4 | ## Documentation
5 | * Marco Zanon
6 | * David Taber
7 |
8 | ## Translations
9 | * de: Sebastian Jentschke
10 | * en-US: John Bieling, Marco Zanon
11 | * it: Marco Zanon
12 | * nb-NO: Sebastian Jentschke
13 | * nl: Jeroen B.
14 |
15 | ## Other contributions
16 | * John Bieling (original TbSync add-on template and advices)
17 | * All the nice people who reported issues and helped beta-testing, fixing bugs and improving the documentation
18 |
--------------------------------------------------------------------------------
/background.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | await browser.LegacyHelper.registerGlobalUrls([
11 | ["content", "google-4-tbsync", "content/"],
12 | ["resource", "google-4-tbsync", "."],
13 | ]);
14 |
15 | await browser.Google4TbSync.load();
16 |
--------------------------------------------------------------------------------
/content/locales.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 | var { ExtensionParent } = ChromeUtils.importESModule(
10 | "resource://gre/modules/ExtensionParent.sys.mjs"
11 | );
12 | var tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
13 | "tbsync@jobisoft.de"
14 | );
15 | var { TbSync } = ChromeUtils.importESModule(
16 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
17 | );
18 |
19 | TbSync.localizeOnLoad(window, "google");
20 |
--------------------------------------------------------------------------------
/content/manager/authenticate.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/content/includes/NetworkError.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | class NetworkError extends Error { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
13 |
14 | /* */
15 |
16 | constructor(...args) {
17 | // Pass the arguments (including vendor specific ones) to parent constructor.
18 | super(...args);
19 | // Maintain proper stack trace for where our error was thrown from (only available on V8).
20 | if (Error.captureStackTrace) {
21 | Error.captureStackTrace(this, NetworkError);
22 | }
23 | // Set the internal name.
24 | this.name = "NetworkError";
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/content/includes/ResponseError.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | class ResponseError extends Error { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
13 |
14 | /* */
15 |
16 | constructor(...args) {
17 | // Pass the arguments (including vendor specific ones) to parent constructor.
18 | super(...args);
19 | // Maintain proper stack trace for where our error was thrown from (only available on V8).
20 | if (Error.captureStackTrace) {
21 | Error.captureStackTrace(this, ResponseError);
22 | }
23 | // Set the internal name.
24 | this.name = "ResponseError";
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/content/includes/IllegalArgumentError.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | class IllegalArgumentError extends Error { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
13 |
14 | /* */
15 |
16 | constructor(...args) {
17 | // Pass the arguments (including vendor specific ones) to parent constructor.
18 | super(...args);
19 | // Maintain proper stack trace for where our error was thrown from (only available on V8).
20 | if (Error.captureStackTrace) {
21 | Error.captureStackTrace(this, IllegalArgumentError);
22 | }
23 | // Set the internal name.
24 | this.name = "IllegalArgumentError";
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/content/includes/AuthorizationCodeError.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | class AuthorizationCodeError extends Error { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
13 |
14 | /* */
15 |
16 | constructor(...args) {
17 | // Pass the arguments (including vendor specific ones) to parent constructor.
18 | super(...args);
19 | // Maintain proper stack trace for where our error was thrown from (only available on V8).
20 | if (Error.captureStackTrace) {
21 | Error.captureStackTrace(this, AuthorizationCodeError);
22 | }
23 | // Set the internal name.
24 | this.name = "AuthorizationCodeError";
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/api/LegacyHelper/schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "namespace": "LegacyHelper",
4 | "functions": [
5 | {
6 | "name": "registerGlobalUrls",
7 | "type": "function",
8 | "async": true,
9 | "description": "Register folders which should be available as legacy chrome:// urls or resource:// urls",
10 | "parameters": [
11 | {
12 | "name": "data",
13 | "type": "array",
14 | "items": {
15 | "type": "array",
16 | "items": {
17 | "type": "string"
18 | }
19 | },
20 | "description": "Array of manifest url definitions (content, locale or resource)"
21 | }
22 | ]
23 | },
24 | {
25 | "name": "openDialog",
26 | "type": "function",
27 | "parameters": [
28 | {
29 | "name": "name",
30 | "type": "string",
31 | "description": "name of the new dialog"
32 | },
33 | {
34 | "name": "path",
35 | "type": "string",
36 | "description": "path of the dialog to be opened"
37 | }
38 | ]
39 | }
40 | ]
41 | }
42 | ]
--------------------------------------------------------------------------------
/_locales/nb-NO/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Google-4-TbSync"
4 | },
5 | "extensionDescription": {
6 | "message": "Dette add-on legger til Google-synkroniseringsfunksjoner i TbSync. Bare kontakter og kontaktgrupper administreres for øyeblikket ved hjelp av Googles People API."
7 | },
8 |
9 | "menu.name": {
10 | "message": "Googles People API"
11 | },
12 |
13 | "add.account.title": {
14 | "message": "Skriv inn informasjonen for den nye kontoen"
15 | },
16 | "add.title": {
17 | "message": "Legge til en Google-konto i TbSync"
18 | },
19 |
20 | "manager.tabs.accountsettings": {
21 | "message": "Kontoinnstillinger"
22 | },
23 | "manager.tabs.syncsettings": {
24 | "message": "Instillinger"
25 | },
26 |
27 | "pref.accountName": {
28 | "message": "Kontonavn"
29 | },
30 | "pref.checkConnection": {
31 | "message": "Sjekk tilkoblingen"
32 | },
33 | "pref.clientID": {
34 | "message": "Klient-ID"
35 | },
36 | "pref.clientSecret": {
37 | "message": "Klient-passord"
38 | },
39 | "pref.includeSystemContactGroups": {
40 | "message": "Inkluder systemkontaktgrupper"
41 | },
42 | "pref.readOnlyMode": {
43 | "message": "Skrivebeskyttet modus"
44 | },
45 | "pref.useFakeEmailAddresses": {
46 | "message": "Bruk genererte (fake) e-postadresser"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/_locales/nl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Google-4-TbSync"
4 | },
5 | "extensionDescription": {
6 | "message": "Deze add-on voegt Google-syncronisatiemogelijkheden toe aan de 'TbSync' add-on. Op dit moment worden alleen contacten en contactgroepen ondersteund, door middel van Google's People API."
7 | },
8 |
9 | "menu.name": {
10 | "message": "Google's People API"
11 | },
12 |
13 | "add.account.title": {
14 | "message": "Voer de gegevens in voor het nieuwe account"
15 | },
16 | "add.title": {
17 | "message": "Een Google account toevoegen aan TbSync"
18 | },
19 |
20 | "manager.tabs.accountsettings": {
21 | "message": "Account-instellingen"
22 | },
23 | "manager.tabs.syncsettings": {
24 | "message": "Opties"
25 | },
26 |
27 | "pref.accountName": {
28 | "message": "Accountnaam"
29 | },
30 | "pref.checkConnection": {
31 | "message": "Controleer verbinding"
32 | },
33 | "pref.clientID": {
34 | "message": "Client ID"
35 | },
36 | "pref.clientSecret": {
37 | "message": "Client secret"
38 | },
39 | "pref.includeSystemContactGroups": {
40 | "message": "Inclusief Systeem contactgroepen"
41 | },
42 | "pref.readOnlyMode": {
43 | "message": "Alleen-lezen modus"
44 | },
45 | "pref.useFakeEmailAddresses": {
46 | "message": "Gebruik nep e-mailadressen"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Google-4-TbSync"
4 | },
5 | "extensionDescription": {
6 | "message": "Dieses Add-On erweitert TbSync um Google-Synchronisierungsfunktionen. Derzeit werden nur Kontakte und Kontaktgruppen mithilfe der People-API von Google verwaltet."
7 | },
8 |
9 | "menu.name": {
10 | "message": "People-API von Google"
11 | },
12 |
13 | "add.account.title": {
14 | "message": "Geben Sie die Informationen für das neue Konto ein"
15 | },
16 | "add.title": {
17 | "message": "Hinzufügen eines Google-Kontos mithilfe von TbSync"
18 | },
19 |
20 | "manager.tabs.accountsettings": {
21 | "message": "Kontoeinstellungen"
22 | },
23 | "manager.tabs.syncsettings": {
24 | "message": "Optionen"
25 | },
26 |
27 | "pref.accountName": {
28 | "message": "Kontobezeichnung"
29 | },
30 | "pref.checkConnection": {
31 | "message": "Überprüfen Sie Ihre Verbindung"
32 | },
33 | "pref.clientID": {
34 | "message": "Client-ID"
35 | },
36 | "pref.clientSecret": {
37 | "message": "Client-Passwort"
38 | },
39 | "pref.includeSystemContactGroups": {
40 | "message": "Inkl. der Systemkontaktgruppen"
41 | },
42 | "pref.readOnlyMode": {
43 | "message": "Schreibgeschützter Modus"
44 | },
45 | "pref.useFakeEmailAddresses": {
46 | "message": "Verwende systemgenerierte (fake) e-Mail-Adressen"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/_locales/en-US/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Google-4-TbSync"
4 | },
5 | "extensionDescription": {
6 | "message": "This provider add-on adds Google synchronization capabilities to TbSync. Only contacts and contact groups are currently managed, using Google's People API."
7 | },
8 |
9 | "menu.name": {
10 | "message": "Google's People API"
11 | },
12 |
13 | "add.account.title": {
14 | "message": "Enter the information for the new account"
15 | },
16 | "add.title": {
17 | "message": "Adding a Google account to TbSync"
18 | },
19 |
20 | "manager.tabs.accountsettings": {
21 | "message": "Account settings"
22 | },
23 | "manager.tabs.syncsettings": {
24 | "message": "Options"
25 | },
26 |
27 | "pref.accountName": {
28 | "message": "Account name"
29 | },
30 | "pref.checkConnection": {
31 | "message": "Check connection"
32 | },
33 | "pref.clientID": {
34 | "message": "Client ID"
35 | },
36 | "pref.clientSecret": {
37 | "message": "Client secret"
38 | },
39 | "pref.includeSystemContactGroups": {
40 | "message": "Include system contact groups"
41 | },
42 | "pref.readOnlyMode": {
43 | "message": "Read-only mode"
44 | },
45 | "pref.useFakeEmailAddresses": {
46 | "message": "Use fake email addresses"
47 | },
48 | "pref.verboseLogging": {
49 | "message": "Verbose logging"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/content/includes/Logger.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | if ("undefined" === typeof IllegalArgumentError) {
13 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/IllegalArgumentError.js", this, "UTF-8");
14 | }
15 |
16 | class Logger {
17 |
18 | /* FIXME: disabled as it is still not fully supported.
19 | _verboseLogging = false;
20 | */
21 |
22 | constructor(verboseLogging) {
23 | // FIXME: the next two lines are necessary as a workaround for a TbSync's bug.
24 | if ("true" === verboseLogging) verboseLogging = true;
25 | if ("false" === verboseLogging) verboseLogging = false;
26 | if ("boolean" != typeof verboseLogging) {
27 | throw new IllegalArgumentError("Invalid 'verboseLogging': not a boolean.");
28 | }
29 | //
30 | this._verboseLogging = verboseLogging;
31 | }
32 |
33 | getVerboseLogging() {
34 | return this._verboseLogging;
35 | }
36 |
37 | /* */
38 |
39 | /* Logging. */
40 |
41 | log0(message) {
42 | console.log(message);
43 | }
44 |
45 | log1(message) {
46 | if (this.getVerboseLogging()) {
47 | this.log0(message);
48 | }
49 | }
50 |
51 | }
52 |
53 | var logger = null;
54 |
--------------------------------------------------------------------------------
/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Google-4-TbSync"
4 | },
5 | "extensionDescription": {
6 | "message": "Questo add-on provider aggiunge funzionalità di sincronizzazione con Google a TbSync. Al momento sono gestiti solo i contatti ed i gruppi di contatti, tramite Google's People API."
7 | },
8 |
9 | "menu.name": {
10 | "message": "Google's People API"
11 | },
12 |
13 | "add.account.title": {
14 | "message": "Inserisci le informazioni per il nuovo account"
15 | },
16 | "add.title": {
17 | "message": "Aggiunta di un account Google a TbSync"
18 | },
19 |
20 | "manager.tabs.accountsettings": {
21 | "message": "Impostazioni dell'account"
22 | },
23 | "manager.tabs.syncsettings": {
24 | "message": "Opzioni"
25 | },
26 |
27 | "pref.accountName": {
28 | "message": "Nome dell'account"
29 | },
30 | "pref.checkConnection": {
31 | "message": "Controlla la connessione"
32 | },
33 | "pref.clientID": {
34 | "message": "ID client"
35 | },
36 | "pref.clientSecret": {
37 | "message": "Chiave segreta client"
38 | },
39 | "pref.includeSystemContactGroups": {
40 | "message": "Includi i gruppi di contatti di sistema"
41 | },
42 | "pref.readOnlyMode": {
43 | "message": "Modalità sola-lettura"
44 | },
45 | "pref.useFakeEmailAddresses": {
46 | "message": "Usa indirizzi email fittizi"
47 | },
48 | "pref.verboseLogging": {
49 | "message": "Log dettagliato"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.9.0",
6 | "author": "Marco Zanon",
7 | "default_locale": "en-US",
8 | "applications": {
9 | "gecko": {
10 | "id": "google-4-tbsync@marcozanon.com",
11 | "strict_min_version": "136.0",
12 | "strict_max_version": "140.*"
13 | }
14 | },
15 | "icons": {
16 | "16": "skin/icon16.png",
17 | "32": "skin/icon32.png",
18 | "64": "skin/icon64.png"
19 | },
20 | "background": {
21 | "type": "module",
22 | "scripts": [
23 | "background.js"
24 | ]
25 | },
26 | "permissions": [],
27 | "experiment_apis": {
28 | "LegacyHelper": {
29 | "schema": "api/LegacyHelper/schema.json",
30 | "parent": {
31 | "scopes": [
32 | "addon_parent"
33 | ],
34 | "paths": [
35 | [
36 | "LegacyHelper"
37 | ]
38 | ],
39 | "script": "api/LegacyHelper/implementation.js"
40 | }
41 | },
42 | "Google4TbSync": {
43 | "schema": "api/Google4TbSync/schema.json",
44 | "parent": {
45 | "scopes": [
46 | "addon_parent"
47 | ],
48 | "paths": [
49 | [
50 | "Google4TbSync"
51 | ]
52 | ],
53 | "script": "api/Google4TbSync/implementation.js"
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/api/LegacyHelper/README.md:
--------------------------------------------------------------------------------
1 | ## Objective
2 |
3 | This API is a temporary helper while converting legacy extensions to modern WebExtensions. It allows to register `resource://` URLs, which are needed to load custom system modules (*.sys.mjs), and `chrome://` URLs, which are needed to open legacy XUL dialogs.
4 |
5 | ## Usage
6 |
7 | Add the [LegacyHelper API](https://github.com/thunderbird/webext-support/tree/master/experiments/LegacyHelper) to your add-on. Your `manifest.json` needs an entry like this:
8 |
9 | ```json
10 | "experiment_apis": {
11 | "LegacyHelper": {
12 | "schema": "api/LegacyHelper/schema.json",
13 | "parent": {
14 | "scopes": ["addon_parent"],
15 | "paths": [["LegacyHelper"]],
16 | "script": "api/LegacyHelper/implementation.js"
17 | }
18 | }
19 | },
20 | ```
21 |
22 | ## API Functions
23 |
24 | This API provides the following functions:
25 |
26 | ### async registerGlobalUrls(data)
27 |
28 | Register `chrome://*/content/` and `resource://*/` URLs. The function accepts a `data` parameter, which is an array of URL definition items. For example:
29 |
30 | ```javascript
31 | await browser.LegacyHelper.registerGlobalUrls([
32 | ["content", "myaddon", "chrome/content/"],
33 | ["resource", "myaddon", "modules/"],
34 | ]);
35 | ```
36 |
37 | This registers the following URLs:
38 | * `chrome://myaddon/content/` pointing to the `/chrome/content/` folder (the `/content/` part in the URL is fix and does not depend on the name of the folder it is pointing to)
39 | * `resource://myaddon/` pointing to the `/modules/` folder. To register a `resource://` URL which points to the root folder, use `.` instead".
40 |
41 | ### async openDialog(name, path)
42 |
43 | Open a XUL dialog. The `name` parameter is a unique name identifying the dialog. If the dialog with that name is already open, it will be focused instead of being re-opened. The `path` parameter is a `chrome://*/content/` URL pointing to the XUL dialog file (*.xul or *.xhtml).
44 |
45 | ```javascript
46 | browser.LegacyHelper.openDialog(
47 | "XulAddonOptions",
48 | "chrome://myaddon/content/options.xhtml"
49 | );
50 | ```
51 |
--------------------------------------------------------------------------------
/api/Google4TbSync/implementation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This Source Code Form is subject to the terms of the Mozilla Public
3 | * License, v. 2.0. If a copy of the MPL was not distributed with this
4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | */
6 |
7 | "use strict";
8 |
9 | // Using a closure to not leak anything but the API to the outside world.
10 | (function (exports) {
11 |
12 | const { ExtensionParent } = ChromeUtils.importESModule(
13 | "resource://gre/modules/ExtensionParent.sys.mjs"
14 | );
15 |
16 | async function observeTbSyncInitialized(aSubject, aTopic, aData) {
17 | try {
18 | const tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
19 | "tbsync@jobisoft.de"
20 | );
21 | const { TbSync } = ChromeUtils.importESModule(
22 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
23 | );
24 | // Load this provider add-on into TbSync
25 | if (TbSync.enabled) {
26 | const googleExtension = ExtensionParent.GlobalManager.getExtension(
27 | "google-4-tbsync@marcozanon.com"
28 | );
29 | console.log(`Registering GOOGLE provider v${googleExtension.manifest.version} with TbSync v${tbsyncExtension.manifest.version}`);
30 | await TbSync.providers.loadProvider(googleExtension, "google", "chrome://google-4-tbsync/content/provider.js");
31 | }
32 | } catch (e) {
33 | // If this fails, TbSync is not loaded yet and we will get the notification later again.
34 | }
35 |
36 | }
37 |
38 | var Google4TbSync = class extends ExtensionCommon.ExtensionAPI {
39 |
40 | getAPI(context) {
41 | return {
42 | Google4TbSync: {
43 | async load() {
44 | Services.obs.addObserver(observeTbSyncInitialized, "tbsync.observer.initialized", false);
45 |
46 | // Did we miss the observer?
47 | observeTbSyncInitialized();
48 | }
49 | },
50 | };
51 | }
52 |
53 | onShutdown(isAppShutdown) {
54 | if (isAppShutdown) {
55 | return; // the application gets unloaded anyway
56 | }
57 |
58 | Services.obs.removeObserver(observeTbSyncInitialized, "tbsync.observer.initialized");
59 | //unload this provider add-on from TbSync
60 | try {
61 | const tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
62 | "tbsync@jobisoft.de"
63 | );
64 | const { TbSync } = ChromeUtils.importESModule(
65 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
66 | );
67 | TbSync.providers.unloadProvider("google");
68 | } catch (e) {
69 | //if this fails, TbSync has been unloaded already and has unloaded this addon as well
70 | }
71 | }
72 | };
73 | exports.Google4TbSync = Google4TbSync;
74 | })(this);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Google-4-TbSync
2 |
3 | This provider add-on adds Google synchronization capabilities to [TbSync](https://github.com/jobisoft/TbSync).
4 |
5 | Only contacts and contact groups are currently managed, using Google's People API. There's currently no plan on supporting calendars. Please see [FAQ](https://github.com/zanonmark/Google-4-TbSync/wiki/FAQ-(Frequently-Asked-Questions)) for details.
6 |
7 | The work is partly based on [EteSync4TbSync](https://github.com/etesync/EteSync-4-TbSync), [DAV4TbSync](https://github.com/jobisoft/DAV-4-TbSync), [gContactSync](https://github.com/jdgeenen/gcontactsync) and many advices by [John Bieling](https://github.com/jobisoft) himself.
8 |
9 | ## Current status and roadmap / Known limitations
10 |
11 | What already works:
12 | * Google-to-Thunderbird creation / update / deletion of contacts;
13 | * Google-to-Thunderbird creation / update / deletion of contact groups;
14 | * Google-to-Thunderbird creation / update / deletion of contact group members.
15 | * Thunderbird-to-Google creation / update / deletion of contacts;
16 | * Thunderbird-to-Google creation / update / deletion of contact groups;
17 |
18 | What is missing:
19 | * Thunderbird-to-Google creation / update / deletion of contact group members. Please note that for this to be fixed the undergoing port of TbSync to WebExtension must be completed first: only then this add-on will be partially rewritten and will be able to fully manage contact group memberships.
20 |
21 | A full working version could probably be ready by mid 2025.
22 |
23 | **Thunderbird 102+ users please note**. Google-4-TbSync 0.4.x runs much slower than 0.3.x (in my tests it performs 7x slower!). This is a known issue, please see [FAQ](https://github.com/zanonmark/Google-4-TbSync/wiki/FAQ-(Frequently-Asked-Questions)) for details. Upgrading to 0.5.x will greatly improve things, especially when updating an addressbook.
24 |
25 | ## How to use it
26 |
27 | You first need to [install TbSync](https://addons.thunderbird.net/addon/tbsync) and [generate your own Google Cloud Console project credentials](https://github.com/zanonmark/Google-4-TbSync/wiki/How-to-generate-your-own-Google-Cloud-Console-project-credentials). Then do one of the following:
28 |
29 | ### Download an official release
30 |
31 | .xpi packages can be downloaded from [Thunderbird Add-ons](https://addons.thunderbird.net/addon/google-4-tbsync), or through the _Thunderbird_ > _Tools_ > _Add-ons_ menu.
32 |
33 | ### Test the latest code
34 |
35 | 1. [Grab the latest .zip package](https://github.com/zanonmark/Google-4-TbSync/archive/refs/heads/main.zip).
36 | 2. Unzip it wherever you want.
37 | 3. Load it as a temporary add-on from _Thunderbird_ > _Tools_ > _Add-ons_ > cog icon > _Debug Add-ons_ > _Load Temporary Add-on_ (pick _manifest.json_ for example).
38 | 4. Test it, preferably using the _Read-only mode_ option (see below).
39 |
40 | ## Warning
41 |
42 | * Even if early reports seem to confirm the add-on is working properly, the project is still in its early development stage: **do regular backups of both your Google and Thunderbird address books!**
43 | * **You are strongly suggested to use the [_Read-only mode_ option](https://github.com/zanonmark/Google-4-TbSync/wiki/Account-options#read-only-mode)**.
44 |
45 | ## Additional information
46 |
47 | Please refer to the [wiki section](https://github.com/zanonmark/Google-4-TbSync/wiki) for other useful information, including [FAQ](https://github.com/zanonmark/Google-4-TbSync/wiki/FAQ-(Frequently-Asked-Questions)), guides and user contributions.
48 |
--------------------------------------------------------------------------------
/api/LegacyHelper/implementation.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is provided by the webext-support repository at
3 | * https://github.com/thunderbird/webext-support
4 | *
5 | * Version 1.1
6 | * - registerGlobalUrls() is now async, to be able to properly await registration
7 | *
8 | * Version 1.0
9 | * - initial release
10 | *
11 | * Author:
12 | * - John Bieling (john@thunderbird.net)
13 | *
14 | * This Source Code Form is subject to the terms of the Mozilla Public
15 | * License, v. 2.0. If a copy of the MPL was not distributed with this
16 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
17 | */
18 |
19 | "use strict";
20 |
21 | // Using a closure to not leak anything but the API to the outside world.
22 | (function (exports) {
23 |
24 | const aomStartup = Cc[
25 | "@mozilla.org/addons/addon-manager-startup;1"
26 | ].getService(Ci.amIAddonManagerStartup);
27 | const resProto = Cc[
28 | "@mozilla.org/network/protocol;1?name=resource"
29 | ].getService(Ci.nsISubstitutingProtocolHandler);
30 |
31 | const chromeHandlers = [];
32 | const resourceUrls = [];
33 |
34 | var LegacyHelper = class extends ExtensionCommon.ExtensionAPI {
35 | getAPI(context) {
36 | return {
37 | LegacyHelper: {
38 | registerGlobalUrls(data) {
39 | const manifestURI = Services.io.newURI(
40 | "manifest.json",
41 | null,
42 | context.extension.rootURI
43 | );
44 |
45 | for (let entry of data) {
46 | // [ "resource", "shortname" , "path" ]
47 |
48 | switch (entry[0]) {
49 | case "resource":
50 | {
51 | let uri = Services.io.newURI(
52 | entry[2],
53 | null,
54 | context.extension.rootURI
55 | );
56 | resProto.setSubstitutionWithFlags(
57 | entry[1],
58 | uri,
59 | resProto.ALLOW_CONTENT_ACCESS
60 | );
61 | resourceUrls.push(entry[1]);
62 | }
63 | break;
64 |
65 | case "content":
66 | case "locale":
67 | {
68 | let handle = aomStartup.registerChrome(
69 | manifestURI,
70 | [entry]
71 | );
72 | chromeHandlers.push(handle);
73 | }
74 | break;
75 |
76 | default:
77 | console.warn(`LegacyHelper: Unsupported url type: ${entry[0]}`)
78 | }
79 | }
80 | },
81 |
82 | openDialog(name, path) {
83 | let window = Services.wm.getMostRecentWindow("mail:3pane");
84 | window.openDialog(
85 | path,
86 | name,
87 | "chrome,resizable,centerscreen"
88 | );
89 | },
90 | },
91 | };
92 | }
93 |
94 | onShutdown(isAppShutdown) {
95 | if (isAppShutdown) {
96 | return; // the application gets unloaded anyway
97 | }
98 |
99 | for (let chromeHandler of chromeHandlers) {
100 | if (chromeHandler) {
101 | chromeHandler.destruct();
102 | chromeHandler = null;
103 | }
104 | }
105 |
106 | for (let resourceUrl of resourceUrls) {
107 | resProto.setSubstitution(
108 | resourceUrl,
109 | null
110 | );
111 | }
112 |
113 | // Flush all caches.
114 | Services.obs.notifyObservers(null, "startupcache-invalidate");
115 | }
116 | };
117 | exports.LegacyHelper = LegacyHelper;
118 | })(this);
--------------------------------------------------------------------------------
/content/manager/editAccountOverlay.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 | var { ExtensionParent } = ChromeUtils.importESModule(
12 | "resource://gre/modules/ExtensionParent.sys.mjs"
13 | );
14 | var tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
15 | "tbsync@jobisoft.de"
16 | );
17 | var { TbSync } = ChromeUtils.importESModule(
18 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
19 | );
20 | const google = TbSync.providers.google;
21 |
22 | var tbSyncEditAccountOverlay = {
23 |
24 | accountNameWidget: null,
25 | clientIDWidget: null,
26 | clientSecretWidget: null,
27 | includeSystemContactGroupsWidget: null,
28 | useFakeEmailAddressesWidget: null,
29 | readOnlyModeWidget: null,
30 | verboseLoggingWidget: null,
31 | /*
32 | checkConnectionWidget: null,
33 | */
34 |
35 | onload: function(window, accountData) {
36 | this.accountData = accountData;
37 | //
38 | this.accountNameWidget = document.getElementById("tbsync.accountsettings.pref.accountname");
39 | this.clientIDWidget = document.getElementById("tbsync.accountsettings.pref.clientID");
40 | this.clientSecretWidget = document.getElementById("tbsync.accountsettings.pref.clientSecret");
41 | this.includeSystemContactGroupsWidget = document.getElementById('tbsync.accountsettings.pref.includeSystemContactGroups');
42 | this.useFakeEmailAddressesWidget = document.getElementById('tbsync.accountsettings.pref.useFakeEmailAddresses');
43 | this.readOnlyModeWidget = document.getElementById('tbsync.accountsettings.pref.readOnlyMode');
44 | this.verboseLoggingWidget = document.getElementById('tbsync.accountsettings.pref.verboseLogging');
45 | //
46 | this.accountNameWidget.value = this.accountData.getAccountProperty("accountname");
47 | this.clientIDWidget.value = this.accountData.getAccountProperty("clientID");
48 | this.clientSecretWidget.value = this.accountData.getAccountProperty("clientSecret");
49 | this.includeSystemContactGroupsWidget.checked = this.accountData.getAccountProperty("includeSystemContactGroups");
50 | this.useFakeEmailAddressesWidget.checked = this.accountData.getAccountProperty("useFakeEmailAddresses");
51 | this.readOnlyModeWidget.checked = this.accountData.getAccountProperty("readOnlyMode");
52 | this.verboseLoggingWidget.checked = this.accountData.getAccountProperty("verboseLogging");
53 | },
54 |
55 | updateAccountProperty: function(accountProperty) {
56 | switch (accountProperty) {
57 | case "accountName":
58 | this.accountData.setAccountProperty("accountname", this.accountNameWidget.value);
59 | break;
60 | case "clientID":
61 | this.accountData.setAccountProperty("clientID", this.clientIDWidget.value);
62 | break;
63 | case "clientSecret":
64 | this.accountData.setAccountProperty("clientSecret", this.clientSecretWidget.value);
65 | break;
66 | case "includeSystemContactGroups":
67 | this.accountData.setAccountProperty("includeSystemContactGroups", this.includeSystemContactGroupsWidget.checked);
68 | break;
69 | case "useFakeEmailAddresses":
70 | this.accountData.setAccountProperty("useFakeEmailAddresses", this.useFakeEmailAddressesWidget.checked);
71 | break;
72 | case "readOnlyMode":
73 | this.accountData.setAccountProperty("readOnlyMode", this.readOnlyModeWidget.checked);
74 | break;
75 | case "verboseLogging":
76 | this.accountData.setAccountProperty("verboseLogging", this.verboseLoggingWidget.checked);
77 | break;
78 | default:
79 | break;
80 | }
81 | },
82 |
83 | onCheckConnection: function() {
84 | let accountData = this.accountData;
85 | //
86 | let peopleAPI = new PeopleAPI(accountData);
87 | //
88 | peopleAPI.checkConnection();
89 | },
90 |
91 | };
92 |
--------------------------------------------------------------------------------
/content/manager/createAccount.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | var { ExtensionParent } = ChromeUtils.importESModule(
13 | "resource://gre/modules/ExtensionParent.sys.mjs"
14 | );
15 | var tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
16 | "tbsync@jobisoft.de"
17 | );
18 | var { TbSync } = ChromeUtils.importESModule(
19 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
20 | );
21 | const google = TbSync.providers.google;
22 |
23 | var tbSyncNewAccount = {
24 |
25 | accountNameWidget: null,
26 | clientIDWidget: null,
27 | clientSecretWidget: null,
28 | includeSystemContactGroupsWidget: null,
29 | useFakeEmailAddressesWidget: null,
30 | readOnlyModeWidget: null,
31 | verboseLoggingWidget: null,
32 |
33 | onLoad: function () {
34 | this.providerData = new TbSync.ProviderData("google");
35 | //
36 | document.getElementById('tbsync.newaccount.wizard')._adjustWizardHeader(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1618252
37 | document.getElementById("firstPage").label = TbSync.getString("__GOOGLE4TBSYNCMSG_add.account.title__", "google");
38 | //
39 | this.accountNameWidget = document.getElementById("tbsync.newaccount.accountName");
40 | this.clientIDWidget = document.getElementById("tbsync.newaccount.clientID");
41 | this.clientSecretWidget = document.getElementById("tbsync.newaccount.clientSecret");
42 | this.includeSystemContactGroupsWidget = document.getElementById('tbsync.newaccount.includeSystemContactGroups');
43 | this.useFakeEmailAddressesWidget = document.getElementById('tbsync.newaccount.useFakeEmailAddresses');
44 | this.readOnlyModeWidget = document.getElementById('tbsync.newaccount.readOnlyMode');
45 | this.verboseLoggingWidget = document.getElementById('tbsync.newaccount.verboseLogging');
46 | //
47 | this.accountNameWidget.value = "";
48 | this.clientIDWidget.value = "";
49 | this.clientSecretWidget.value = "";
50 | this.includeSystemContactGroupsWidget.checked = false;
51 | this.useFakeEmailAddressesWidget.checked = false;
52 | this.readOnlyModeWidget.checked = true;
53 | this.verboseLoggingWidget.checked = false;
54 | //
55 | document.getElementById("tbsync.newaccount.wizard").canRewind = false;
56 | document.getElementById("tbsync.newaccount.wizard").canAdvance = false;
57 | //
58 | this.accountNameWidget.focus();
59 | //
60 | document.addEventListener("wizardfinish", tbSyncNewAccount.onFinish.bind(this));
61 | },
62 |
63 | onUnload: function () {
64 | },
65 |
66 | onClose: function () {
67 | return true;
68 | },
69 |
70 | onUserTextInput: function () {
71 | document.getElementById("tbsync.newaccount.wizard").canAdvance = (("" !== this.accountNameWidget.value.trim()) && ("" !== this.clientIDWidget.value.trim()) && ("" !== this.clientSecretWidget.value.trim()));
72 | },
73 |
74 | onFinish: function (event) {
75 | let accountName = this.accountNameWidget.value.trim();
76 | let clientID = this.clientIDWidget.value.trim();
77 | let clientSecret = this.clientSecretWidget.value.trim();
78 | let includeSystemContactGroups = this.includeSystemContactGroupsWidget.checked;
79 | let useFakeEmailAddresses = this.useFakeEmailAddressesWidget.checked;
80 | let readOnlyMode = this.readOnlyModeWidget.checked;
81 | let verboseLogging = this.verboseLoggingWidget.checked;
82 | //
83 | tbSyncNewAccount.addAccount(accountName, clientID, clientSecret, includeSystemContactGroups, useFakeEmailAddresses, readOnlyMode, verboseLogging);
84 | },
85 |
86 | addAccount: function (accountName, clientID, clientSecret, includeSystemContactGroups, useFakeEmailAddresses, readOnlyMode, verboseLogging) {
87 | // Retrieve a new object with default values.
88 | let newAccountEntry = this.providerData.getDefaultAccountEntries();
89 | // Override the default values.
90 | newAccountEntry.clientID = clientID;
91 | newAccountEntry.clientSecret = clientSecret;
92 | newAccountEntry.includeSystemContactGroups = includeSystemContactGroups;
93 | newAccountEntry.useFakeEmailAddresses = useFakeEmailAddresses;
94 | newAccountEntry.readOnlyMode = readOnlyMode;
95 | newAccountEntry.verboseLogging = verboseLogging;
96 | // Add the new account.
97 | let newAccountData = this.providerData.addAccount(accountName, newAccountEntry);
98 | // Close the window.
99 | window.close();
100 | },
101 |
102 | };
103 |
--------------------------------------------------------------------------------
/content/manager/createAccount.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
--------------------------------------------------------------------------------
/content/includes/sync.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | var { ExtensionParent } = ChromeUtils.importESModule(
13 | "resource://gre/modules/ExtensionParent.sys.mjs"
14 | );
15 | var tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
16 | "tbsync@jobisoft.de"
17 | );
18 | var { TbSync } = ChromeUtils.importESModule(
19 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
20 | );
21 |
22 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/AddressBookSynchronizer.js", this, "UTF-8");
23 | if ("undefined" === typeof NetworkError) {
24 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/NetworkError.js", this, "UTF-8");
25 | }
26 |
27 | var sync = {
28 |
29 | finish: function(aStatus = "", msg = "", details = "") {
30 | let status = TbSync.StatusData.SUCCESS;
31 | //
32 | switch (aStatus) {
33 | case "":
34 | case "ok":
35 | status = TbSync.StatusData.SUCCESS;
36 | //
37 | break;
38 | case "info":
39 | status = TbSync.StatusData.INFO;
40 | //
41 | break;
42 | case "resyncAccount":
43 | status = TbSync.StatusData.ACCOUNT_RERUN;
44 | //
45 | break;
46 | case "resyncFolder":
47 | status = TbSync.StatusData.FOLDER_RERUN;
48 | //
49 | break;
50 | case "warning":
51 | status = TbSync.StatusData.WARNING;
52 | //
53 | break;
54 | case "error":
55 | status = TbSync.StatusData.ERROR;
56 | //
57 | break;
58 | default:
59 | console.log("Google-4-TbSync: Unknown status <" + aStatus + ">");
60 | status = TbSync.StatusData.ERROR;
61 | //
62 | break;
63 | }
64 | //
65 | let e = new Error();
66 | e.name = "google-4-tbsync";
67 | e.message = status.toUpperCase() + ": " + msg.toString() + " (" + details.toString() + ")";
68 | e.statusData = new TbSync.StatusData(status, msg.toString(), details.toString());
69 | //
70 | return e;
71 | },
72 |
73 | folderList: async function(syncData) {
74 | try {
75 | // Retrieve information about the authenticated user.
76 | let peopleAPI = new PeopleAPI(syncData.accountData);
77 | let authenticatedUser = await peopleAPI.getAuthenticatedUser();
78 | let authenticatedUserEmail = authenticatedUser.emailAddresses[0].value;
79 | // Simulation of folders retrieved from Server.
80 | let foundFolders = [
81 | {UID: 1, name: authenticatedUserEmail},
82 | ];
83 | //
84 | for (let folder of foundFolders) {
85 | let existingFolder = syncData.accountData.getFolder("UID", folder.UID);
86 | //
87 | if (existingFolder) {
88 | // We know this folder, update changed properties.
89 | // foldername is a default FolderProperty.
90 | existingFolder.setFolderProperty("foldername", folder.name);
91 | }
92 | else {
93 | // Create the folder object for the new folder settings.
94 | let newFolder = syncData.accountData.createNewFolder();
95 | // foldername is a default FolderProperty.
96 | newFolder.setFolderProperty("foldername", folder.name);
97 | // targetType is a default FolderProperty and is used to select a TargetData implementation.
98 | newFolder.setFolderProperty("targetType", "addressbook");
99 | // UID is a custom FolderProperty (defined at getDefaultFolderEntries).
100 | newFolder.setFolderProperty("UID", folder.UID);
101 | // Do we have a cached folder?
102 | let cachedFolderData = syncData.accountData.getFolderFromCache("UID", folder.UID);
103 | if (cachedFolderData) {
104 | // Copy fields from cache which we want to re-use.
105 | newFolder.setFolderProperty("downloadonly", cachedFolderData.getFolderProperty("downloadonly"));
106 | }
107 | }
108 | }
109 | }
110 | catch (error) {
111 | // If a network error was encountered...
112 | if (error instanceof NetworkError) {
113 | console.log("sync.folderList(): Network error.");
114 | // Propagate the error.
115 | throw error;
116 | }
117 | // If the root reason is different...
118 | else {
119 | // Propagate the error.
120 | throw error;
121 | }
122 | }
123 | },
124 |
125 | singleFolder: async function(syncData) {
126 | // Add target to syncData.
127 | try {
128 | // Accessing the target for the first time will check if it is available and if not will create it (if possible).
129 | syncData.target = await syncData.currentFolderData.targetData.getTarget();
130 | }
131 | catch (e) {
132 | Components.utils.reportError(e);
133 | throw google.sync.finish("warning", e.message);
134 | }
135 | //
136 | syncData.setSyncState("preparing");
137 | //
138 | try {
139 | switch (syncData.currentFolderData.getFolderProperty("targetType")) {
140 | case "addressbook":
141 | await AddressBookSynchronizer.synchronize(syncData);
142 | //
143 | break;
144 | default:
145 | throw new Error("Unsupported target");
146 | //
147 | break;
148 | }
149 | }
150 | catch (e) {
151 | Components.utils.reportError(e);
152 | throw google.sync.finish("warning", e.message);
153 | }
154 | },
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/content/manager/editAccountOverlay.xhtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | __TBSYNCMSG_manager.lockedsettings.description__
127 |
128 |
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/content/provider.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | var { ExtensionParent } = ChromeUtils.importESModule(
13 | "resource://gre/modules/ExtensionParent.sys.mjs"
14 | );
15 | var { MailServices } = ChromeUtils.importESModule(
16 | "resource:///modules/MailServices.sys.mjs"
17 | );
18 | var tbsyncExtension = ExtensionParent.GlobalManager.getExtension(
19 | "tbsync@jobisoft.de"
20 | );
21 | var { TbSync } = ChromeUtils.importESModule(
22 | `chrome://tbsync/content/tbsync.sys.mjs?${tbsyncExtension.manifest.version}`
23 | );
24 |
25 | // Every Object in here will be loaded into the following namespace: TbSync.providers.google.
26 | const google = TbSync.providers.google;
27 |
28 | /**
29 | * Base class for the TbSync provider interface.
30 | */
31 | var Base = class {
32 |
33 | /**
34 | * Called during the load of this provider add-on.
35 | *
36 | */
37 | static async load() {
38 | // Set the default preferences.
39 | let branch = Services.prefs.getDefaultBranch("extensions.google-4-tbsync.");
40 | branch.setIntPref("timeout", 50);
41 | }
42 |
43 | /**
44 | * Called during the unload of this provider add-on.
45 | *
46 | */
47 | static async unload() {
48 | }
49 |
50 | /**
51 | * Returns the name of this provider for the "Add account" menu of the
52 | * TbSync account manager.
53 | *
54 | * @returns {string} A name.
55 | *
56 | */
57 | static getProviderName() {
58 | return TbSync.getString("menu.name", "google");
59 | }
60 |
61 | /**
62 | * Returns the version identifier of the TbSync API this provider is using.
63 | * If it is not matching the version identifier of the TbSync add-on the
64 | * user has currently installed, this provider add-on is not loaded.
65 | *
66 | * @returns {string} A version identifier.
67 | *
68 | */
69 | static getApiVersion() {
70 | return "2.5";
71 | }
72 |
73 | /**
74 | * Returns the location of an icon for this provider.
75 | *
76 | * @param {integer} size The size of the requested icon.
77 | * @param {AccountData} accountData The AccountData instance of the
78 | * account, which is requesting the
79 | * icon. Optional.
80 | *
81 | * @returns {string} A resource URI to the file to be used as icon.
82 | *
83 | */
84 | static getProviderIcon(size, accountData = null) {
85 | switch (size) {
86 | case 16:
87 | return "resource://google-4-tbsync/skin/icon16.png";
88 | case 32:
89 | return "resource://google-4-tbsync/skin/icon32.png";
90 | default:
91 | return "resource://google-4-tbsync/skin/icon64.png";
92 | }
93 | }
94 |
95 | /**
96 | * Returns a list of sponsors, sorted by sortIndex.
97 | *
98 | * ::
99 | *
100 | * return {
101 | * "sortIndex": {
102 | * name: "Name",
103 | * description: "Something",
104 | * icon: "chrome://path/or/empty",
105 | * link: "url://or/empty",
106 | * },
107 | * }
108 | *
109 | * @returns {Object} A list of sponsors.
110 | *
111 | */
112 | static getSponsors() {
113 | return {};
114 | }
115 |
116 | /**
117 | * Returns the URL of a page with details about contributors (used in the
118 | * manager UI).
119 | *
120 | * @returns {string} An URL.
121 | *
122 | */
123 | static getContributorsUrl() {
124 | return "https://github.com/zanonmark/Google-4-TbSync/blob/master/CONTRIBUTORS.md";
125 | }
126 |
127 | /**
128 | * Returns the email address of the maintainer (used for bug reports).
129 | *
130 | * @returns {string} An email address.
131 | *
132 | */
133 | static getMaintainerEmail() {
134 | return "info@marcozanon.com";
135 | }
136 |
137 | /**
138 | * Returns the URL of the new account window.
139 | *
140 | * The URL will be opened via openDialog() when the user wants to create a
141 | * new account of this provider.
142 | *
143 | * @returns {string} A chrome URI to the file to be used in the "Create
144 | * account" dialog.
145 | *
146 | */
147 | static getCreateAccountWindowUrl() {
148 | return "chrome://google-4-tbsync/content/manager/createAccount.xhtml";
149 | }
150 |
151 | /**
152 | * Returns the URL to the overlay for the "Edit account" dialog
153 | * (chrome://tbsync/content/manager/editAccount.xul)
154 | *
155 | * The overlay must (!) implement:
156 | *
157 | * ``tbSyncEditAccountOverlay.onload(window, accountData)``
158 | *
159 | * which is called each time an account of this provider is viewed/selected
160 | * in the manager and gets passed the AccountData of the corresponding
161 | * account.
162 | *
163 | * @returns {string} A chrome URI to overlay for the "Edit account" dialog.
164 | *
165 | */
166 | static getEditAccountOverlayUrl() {
167 | return "chrome://google-4-tbsync/content/manager/editAccountOverlay.xhtml";
168 | }
169 |
170 | /**
171 | * Returns an Object which contains all possible account properties of
172 | * accounts of this provider, with their default value if not yet stored in
173 | * the database.
174 | *
175 | * The returned Object uses the property names as keys and their default
176 | * values as values:
177 | *
178 | * ::
179 | *
180 | * return {
181 | * username: "",
182 | * host: "",
183 | * https: true,
184 | * someOtherOption: false,
185 | * }
186 | *
187 | * Please also check the standard account properties added by TbSync.
188 | *
189 | * @returns {Object} A list of properties with default values.
190 | *
191 | */
192 | static getDefaultAccountEntries() {
193 | let row = {
194 | clientID: "",
195 | clientSecret: "",
196 | includeSystemContactGroups: false,
197 | useFakeEmailAddresses: false,
198 | readOnlyMode: true,
199 | verboseLogging: false,
200 | refreshToken: null,
201 | };
202 | //
203 | return row;
204 | }
205 |
206 | /**
207 | * Returns an Object which contains all possible folder properties of
208 | * folders of this provider, with their default value if not yet stored in
209 | * the database.
210 | *
211 | * @returns {Object} A list of properties with default values.
212 | *
213 | */
214 | static getDefaultFolderEntries() {
215 | let folder = {
216 | UID: "",
217 | target: "",
218 | targetName: "",
219 | targetType: "",
220 | foldername: "",
221 | downloadonly: "",
222 | };
223 | //
224 | return folder;
225 | }
226 |
227 | /**
228 | * Called everytime an account of this provider is enabled in the manager
229 | * UI.
230 | *
231 | * @param {AccountData} accountData The AccountData instance of the
232 | * account being enabled.
233 | *
234 | */
235 | static onEnableAccount(accountData) {
236 | }
237 |
238 | /**
239 | * Called everytime an account of this provider is disabled in the manager
240 | * UI.
241 | *
242 | * @param {AccountData} accountData The AccountData instance of the
243 | * account being disabled.
244 | *
245 | */
246 | static onDisableAccount(accountData) {
247 | }
248 |
249 | /**
250 | * Called everytime an account of this provider is deleted in the manager
251 | * UI.
252 | *
253 | * @param {AccountData} accountData The AccountData instance of the
254 | * account being deleted.
255 | *
256 | */
257 | static onDeleteAccount(accountData) {
258 | }
259 |
260 | /**
261 | * Implement this method if this provider should add additional entries
262 | * to the autocomplete list while typing something into the address field
263 | * of the message composer.
264 | *
265 | * The return value is an Array of Objects and each Object needs the
266 | * following attributes:
267 | *
268 | * * ``value`` : An email address written like
269 | * ``DisplayName ``.
270 | * * ``comment`` : ``Optional`` A comment displayed to the right of the
271 | * value in the autocomplete list.
272 | * * ``icon`` : ``Optional`` A chrome uri to a 16x16 icon.
273 | * * ``style`` : ``Optional`` A CSS class name.
274 | *
275 | * When creating directories, you can set:
276 | *
277 | * ``directory.setBoolValue("enable_autocomplete", false);``
278 | *
279 | * to disable the default autocomplete for this directory and have full
280 | * control over the autocomplete.
281 | *
282 | * @param {AccountData} accountData The AccountData instance of the
283 | * account being queried.
284 | * @param {string} query The search query.
285 | *
286 | * @returns {array} An array of Objects.
287 | *
288 | */
289 | static async abAutoComplete(accountData, query) {
290 | return [];
291 | }
292 |
293 | /**
294 | * Returns all folders of the account, sorted in the desired order.
295 | *
296 | * The order will be used in the folder list and also as the order to
297 | * synchronize the resources of the account identified by the passed
298 | * AccountData.
299 | *
300 | * @param {AccountData} accountData The AccountData instance for the
301 | * account for which the sorted list of
302 | * folders should be returned.
303 | *
304 | * @returns {array} An array of :class:`FolderData` instances in the
305 | * desired order.
306 | *
307 | */
308 | static getSortedFolders(accountData) {
309 | return accountData.getAllFolders();
310 | }
311 |
312 | /**
313 | * Returns the connection timeout for an active server request, so TbSync
314 | * can append a countdown to the connection timeout, while waiting for an
315 | * answer from the server. Only syncstates which start with ``send.`` will
316 | * trigger this, see :class:`SyncData.setSyncState`.
317 | *
318 | * @param {AccountData} accountData The AccountData instance for the
319 | * account for which the timeout is
320 | * being requested.
321 | *
322 | * @returns {integer} A timeout in milliseconds.
323 | *
324 | */
325 | static getConnectionTimeout(accountData) {
326 | return Services.prefs.getBranch("extensions.google-4-tbsync.").getIntPref("timeout");
327 | }
328 |
329 | /**
330 | * Called to synchronize the folder list.
331 | *
332 | * Never call this method directly, but use :class:`AccountData.sync`.
333 | *
334 | * @param {SyncData} syncData The SyncData instance with information
335 | * regarding the requested synchronization.
336 | * @param {string} syncJob A specific synchronization job, defaults to
337 | * "sync", but can be set via the
338 | * syncDescription (see AccountData.sync or
339 | * FolderData.sync).
340 | * @param {integer} syncRunNr Indicates the n-th number the account is
341 | * being (re-)synchronized due to enforced
342 | * retries. It starts with 1 and is limited by
343 | * syncDescription.maxAccountReruns.
344 | *
345 | * @returns {StatusData} A :class:`StatusData` instance with information of
346 | * the synchronization (failed / success).
347 | *
348 | */
349 | static async syncFolderList(syncData, syncJob, syncRunNr) {
350 | try {
351 | await google.sync.folderList(syncData);
352 | }
353 | catch (e) {
354 | if ("google-4-tbsync" == e.name) {
355 | return e.statusData;
356 | }
357 | else {
358 | Components.utils.reportError(e);
359 | // Re-throw any other error and let TbSync handle it.
360 | throw e;
361 | }
362 | }
363 | //
364 | return new TbSync.StatusData();
365 | }
366 |
367 | /**
368 | * Called to synchronize a folder.
369 | *
370 | * Never call this method directly, but use :class:`AccountData.sync` or
371 | * :class:`FolderData.sync`.
372 | *
373 | * @param {SyncData} syncData The SyncData instance with information
374 | * regarding the requested synchronization.
375 | * @param {string} syncJob A specific synchronization job, defaults to
376 | * "sync", but can be set via the
377 | * syncDescription (see AccountData.sync or
378 | * FolderData.sync).
379 | * @param {integer} syncRunNr Indicates the n-th number the account is
380 | * being (re-)synchronized due to enforced
381 | * retries. It starts with 1 and is limited by
382 | * syncDescription.maxAccountReruns.
383 | *
384 | * @returns {StatusData} A :class:`StatusData` instance with information of
385 | * the synchronization (failed / success).
386 | *
387 | */
388 | static async syncFolder(syncData, syncJob, syncRunNr) {
389 | try {
390 | await google.sync.singleFolder(syncData);
391 | }
392 | catch (e) {
393 | if ("google-4-tbsync" == e.name) {
394 | return e.statusData;
395 | }
396 | else {
397 | Components.utils.reportError(e);
398 | // Re-throw any other error and let TbSync handle it.
399 | throw e;
400 | }
401 | }
402 | //
403 | return new TbSync.StatusData();
404 | }
405 |
406 | }
407 |
408 | /**
409 | * This provider is implementing the StandardFolderList class instead of the
410 | * FolderList class.
411 | */
412 | var StandardFolderList = class {
413 |
414 | /**
415 | * Called before the context menu of the folderlist is shown, allows to
416 | * show / hide custom menu options based on the selected folder. During an
417 | * active synchronization, folderData will be null and the folder list will
418 | * be disabled.
419 | *
420 | * @param {nsIDOMWindow} window The Object of the account settings
421 | * window.
422 | * @param {FolderData} folderData The FolderData instance of the
423 | * selected folder.
424 | *
425 | */
426 | static onContextMenuShowing(window, folderData) {
427 | }
428 |
429 | /**
430 | * Returns the icon for a folder to be shown in the folderlist.
431 | *
432 | * @param {FolderData} folderData The FolderData instance of the folder
433 | * for which the icon is requested.
434 | *
435 | * @returns {string} A Chrome URI of the icon.
436 | *
437 | */
438 | static getTypeImage(folderData) {
439 | switch (folderData.getFolderProperty("targetType")) {
440 | case "addressbook":
441 | return "chrome://tbsync/skin/contacts16.png";
442 | }
443 | }
444 |
445 | /**
446 | * Returns the display name for a folder to be shown in the folderlist.
447 | *
448 | * @param {FolderData} folderData The FolderData instance of the folder
449 | * for which the display name is requested.
450 | *
451 | * @returns {string} A display name of the folder.
452 | *
453 | */
454 | static getFolderDisplayName(folderData) {
455 | return folderData.getFolderProperty("foldername");
456 | }
457 |
458 | /**
459 | * Returns the attributes for the *readonly* `menuitem `_
460 | * element of the ACL selector for a folder to be shown in the folderlist.
461 | * You can define any available attribute (label, disabled, hidden, style,
462 | * ...) by returning an Object which uses the attribute names as keys and
463 | * the attribute values as values. For example:
464 | *
465 | * ::
466 | *
467 | * return {
468 | * label: "Readonly access",
469 | * disabled: false,
470 | * }
471 | *
472 | * If both (RO+RW) do not return any attributes, the ACL menu is not
473 | * displayed at all.
474 | *
475 | * @param {FolderData} folderData The FolderData instance of the folder for
476 | * which the attributes for the ACL RO XUL
477 | * element are requested.
478 | *
479 | * @returns {Object} A list of attributes and their values for the ACL RO
480 | * XUL element.
481 | *
482 | */
483 | static getAttributesRoAcl(folderData) {
484 | return null;
485 | }
486 |
487 | /**
488 | * Returns the attributes for the *read/write*
489 | * `menuitem `_
490 | * element of the ACL selector for a folder to be shown in the folderlist.
491 | * You can define any available attribute (label, disabled, hidden, style,
492 | * ...) by returning an Object which uses the attribute names as keys and
493 | * the attribute values as values. For example:
494 | *
495 | * ::
496 | *
497 | * return {
498 | * label: "Read/Write access",
499 | * disabled: true,
500 | * }
501 | *
502 | * If both (RO+RW) do not return any attributes, the ACL menu is not
503 | * displayed at all.
504 | *
505 | * @param {FolderData} folderData The FolderData instance of the folder
506 | * for which the attributes for the ACL RW
507 | * XUL element are requested.
508 | *
509 | * @returns {Object} A list of attributes and their values for the ACL RW
510 | * XUL element.
511 | *
512 | */
513 | static getAttributesRwAcl(folderData) {
514 | return null;
515 | }
516 |
517 | }
518 |
519 | // See Dav4TbSync's implementation for details.
520 | var TargetData_addressbook = class extends TbSync.addressbook.AdvancedTargetData {
521 |
522 | constructor(folderData) {
523 | super(folderData);
524 | }
525 |
526 | get primaryKeyField() {
527 | return "X-GOOGLE-RESOURCENAME";
528 | }
529 |
530 | generatePrimaryKey() {
531 | return TbSync.generateUUID();
532 | }
533 |
534 | get logUserChanges() {
535 | return true;
536 | }
537 |
538 | directoryObserver(aTopic) {
539 | switch (aTopic) {
540 | case "addrbook-directory-deleted":
541 | case "addrbook-directory-updated":
542 | break;
543 | }
544 | }
545 |
546 | cardObserver(aTopic, abCardItem) {
547 | switch (aTopic) {
548 | case "addrbook-contact-updated":
549 | case "addrbook-contact-deleted":
550 | break;
551 | case "addrbook-contact-created":
552 | break;
553 | }
554 | }
555 |
556 | listObserver(aTopic, abListItem, abListMember) {
557 | switch (aTopic) {
558 | case "addrbook-list-member-added":
559 | case "addrbook-list-member-removed":
560 | break;
561 | case "addrbook-list-deleted":
562 | case "addrbook-list-updated":
563 | break;
564 | case "addrbook-list-created":
565 | break;
566 | }
567 | }
568 |
569 | }
570 |
571 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/sync.js", this, "UTF-8");
572 |
--------------------------------------------------------------------------------
/content/includes/PeopleAPI.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | if ("undefined" === typeof AuthorizationCodeError) {
13 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/AuthorizationCodeError.js", this, "UTF-8");
14 | }
15 | if ("undefined" === typeof IllegalArgumentError) {
16 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/IllegalArgumentError.js", this, "UTF-8");
17 | }
18 | if ("undefined" === typeof Logger) {
19 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/Logger.js", this, "UTF-8");
20 | }
21 | if ("undefined" === typeof NetworkError) {
22 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/NetworkError.js", this, "UTF-8");
23 | }
24 | if ("undefined" === typeof ResponseError) {
25 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/ResponseError.js", this, "UTF-8");
26 | }
27 |
28 | let { MailE10SUtils } = ChromeUtils.importESModule("resource:///modules/MailE10SUtils.sys.mjs");
29 |
30 | const SCOPES = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/contacts"; // https://developers.google.com/people/v1/how-tos/authorizing
31 | const SERVICE_ENDPOINT = "https://people.googleapis.com";
32 | const CONTACT_PERSON_FIELDS = "names,nicknames,emailAddresses,phoneNumbers,addresses,organizations,urls,birthdays,userDefined,imClients,biographies,memberships";
33 | const CONTACT_UPDATE_PERSON_FIELDS = "names,nicknames,emailAddresses,phoneNumbers,addresses,organizations,urls,birthdays,userDefined,imClients,biographies"; // no 'memberships' here
34 | const CONTACT_PAGE_SIZE = 1000;
35 | const CONTACT_GROUP_FIELDS = "name,groupType";
36 | const CONTACT_GROUP_PAGE_SIZE = 1000;
37 |
38 | class PeopleAPI {
39 |
40 | /* FIXME: disabled as it is still not fully supported.
41 | _accountData = null;
42 | */
43 |
44 | /* */
45 |
46 | constructor(accountData) {
47 | if (null == accountData) {
48 | throw new IllegalArgumentError("Invalid 'accountData': null.");
49 | }
50 | //
51 | this._accountData = accountData;
52 | //
53 | if (null == logger) {
54 | logger = new Logger(true);
55 | }
56 | }
57 |
58 | getAccountData() {
59 | return this._accountData;
60 | }
61 |
62 | getClientID() {
63 | return this.getAccountData().getAccountProperty("clientID");
64 | }
65 |
66 | getClientSecret() {
67 | return this.getAccountData().getAccountProperty("clientSecret");
68 | }
69 |
70 | getIncludeSystemContactGroups() {
71 | return this.getAccountData().getAccountProperty("includeSystemContactGroups");
72 | }
73 |
74 | setRefreshToken(refreshToken) {
75 | this.getAccountData().setAccountProperty("refreshToken", refreshToken);
76 | }
77 |
78 | getRefreshToken() {
79 | return this.getAccountData().getAccountProperty("refreshToken");
80 | }
81 |
82 | /* Authentication and authorization. */
83 |
84 | async getNewAuthorizationCode() {
85 | let clientID = this.getClientID();
86 | // Prepare a new promise.
87 | let promise = new Promise(function(resolve, reject) {
88 | // Prepare the authorization code request URL.
89 | let authorizationCodeRequestURL = "https://accounts.google.com/o/oauth2/auth";
90 | authorizationCodeRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
91 | client_id: clientID,
92 | redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
93 | scope: SCOPES,
94 | response_type: "code",
95 | });
96 | logger.log1("PeopleAPI.getNewAuthorizationCode(): authorizationCodeRequestURL = " + authorizationCodeRequestURL);
97 | // Open the browser window and set the event handlers.
98 | let windowWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"].getService(Components.interfaces.nsIWindowWatcher);
99 | let authenticationWindow = windowWatcher.openWindow(null, "chrome://google-4-tbsync/content/manager/authenticate.xhtml", null, "chrome,centerscreen", null);
100 | let browserWidget = null;
101 | let titleInterval = null;
102 | let authorizationCodeRetrieved = false;
103 | authenticationWindow.onload = function() {
104 | // Set the browser widget.
105 | browserWidget = authenticationWindow.document.getElementById("browser");
106 | // Load the URL.
107 | MailE10SUtils.loadURI(browserWidget, authorizationCodeRequestURL);
108 | // Check the response every 1s.
109 | titleInterval = authenticationWindow.setInterval(function() {
110 | // Retrieve the browser title.
111 | let browserTitle = browserWidget.contentTitle;
112 | // If the browser title contains "Success"...
113 | if (browserTitle.startsWith("Success")) {
114 | let pattern = new RegExp("code=(.*)&", "i");
115 | let group = pattern.exec(browserTitle);
116 | // ...and if the authorization code could be retrieved...
117 | if (null != group) {
118 | let authorizationCode = group[1];
119 | logger.log1("PeopleAPI.getNewAuthorizationCode(): authorizationCode = " + authorizationCode);
120 | // Close the browser window.
121 | authenticationWindow.close();
122 | // Stop the title interval.
123 | authenticationWindow.clearInterval(titleInterval);
124 | // Return the authorization code.
125 | authorizationCodeRetrieved = true;
126 | resolve(authorizationCode);
127 | }
128 | }
129 | // If the browser title contains "Error"...
130 | else if (browserTitle.startsWith("Error")) {
131 | // Close the browser window.
132 | authenticationWindow.close();
133 | // Stop the title interval.
134 | authenticationWindow.clearInterval(titleInterval);
135 | // Return an error.
136 | reject(new AuthorizationCodeError("Browser title: " + browserTitle));
137 | }
138 | }, 1000);
139 | };
140 | authenticationWindow.onclose = function() {
141 | // Stop the title interval.
142 | authenticationWindow.clearInterval(titleInterval);
143 | // Return an error if the browser window was closed before retrieving the authorization code.
144 | if (!authorizationCodeRetrieved) {
145 | reject(new AuthorizationCodeError("Browser window closed before the authorization code was retrieved."));
146 | }
147 | }
148 | });
149 | //
150 | return promise;
151 | }
152 |
153 | async getResponseData(method, requestURL, requestData) {
154 | if ((null == method) || ("" === method)) {
155 | throw new IllegalArgumentError("Invalid 'method': null or empty.");
156 | }
157 | if ((null == requestURL) || ("" === requestURL)) {
158 | throw new IllegalArgumentError("Invalid 'requestURL': null or empty.");
159 | }
160 | //
161 | logger.log1("PeopleAPI.getResponseData(): requestURL = " + requestURL);
162 | logger.log1("PeopleAPI.getResponseData(): requestData = " + JSON.stringify(requestData));
163 | // Perform the request.
164 | let response = null;
165 | try {
166 | response = await fetch(requestURL, {
167 | method: method,
168 | headers: {
169 | "Content-Type": "application/json",
170 | },
171 | body: ((null == requestData) ? null : JSON.stringify(requestData)),
172 | });
173 | }
174 | catch (error) {
175 | // If a network error was encountered...
176 | if (("TypeError" === error.name) && (error.message.includes("NetworkError"))) {
177 | throw new NetworkError(error.message);
178 | }
179 | // If the root reason is different...
180 | else {
181 | // Propagate the error.
182 | throw error;
183 | }
184 | }
185 | // Check the response status.
186 | logger.log1("PeopleAPI.getResponseData(): responseStatus = " + response.status);
187 | if (200 != response.status) {
188 | throw new ResponseError("Invalid response: " + response.status + ": " + response.statusText);
189 | }
190 | // Retrieve the response data.
191 | let responseData = await response.json();
192 | logger.log1("PeopleAPI.getResponseData(): responseData = " + JSON.stringify(responseData));
193 | //
194 | return responseData;
195 | }
196 |
197 | async retrieveNewRefreshToken() {
198 | // Get a new authorization code.
199 | let authorizationCode = await this.getNewAuthorizationCode();
200 | // Prepare the refresh token request URL and data.
201 | let refreshTokenRequestURL = "https://accounts.google.com/o/oauth2/token";
202 | let refreshTokenRequestData = {
203 | client_id: this.getClientID(),
204 | client_secret: this.getClientSecret(),
205 | redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
206 | grant_type: "authorization_code",
207 | code: authorizationCode,
208 | };
209 | // Perform the request and retrieve the response data.
210 | let responseData = await this.getResponseData("POST", refreshTokenRequestURL, refreshTokenRequestData);
211 | // Retrieve the refresh token.
212 | let refreshToken = responseData.refresh_token;
213 | logger.log1("PeopleAPI.retrieveNewRefreshToken(): refreshToken = " + refreshToken);
214 | // Save the refresh token to the account data.
215 | this.setRefreshToken(refreshToken);
216 | }
217 |
218 | async getNewAccessToken(retrieveNewRefreshToken = false) {
219 | logger.log1("PeopleAPI.getNewAccessToken(): retrieveNewRefreshToken = " + retrieveNewRefreshToken);
220 | // Retrieve a new refresh token if necessary.
221 | if (retrieveNewRefreshToken) {
222 | await this.retrieveNewRefreshToken();
223 | }
224 | // Get the refresh token.
225 | let refreshToken = this.getRefreshToken();
226 | // Prepare the access token request URL and data.
227 | let accessTokenRequestURL = "https://accounts.google.com/o/oauth2/token";
228 | let accessTokenRequestData = {
229 | client_id: this.getClientID(),
230 | client_secret: this.getClientSecret(),
231 | grant_type: "refresh_token",
232 | refresh_token: refreshToken,
233 | };
234 | // Try retrieving the access token.
235 | try {
236 | // Perform the request and retrieve the response data.
237 | let responseData = await this.getResponseData("POST", accessTokenRequestURL, accessTokenRequestData);
238 | // Retrieve the access token.
239 | let accessToken = responseData.access_token;
240 | logger.log1("PeopleAPI.getNewAccessToken(): accessToken = " + accessToken);
241 | //
242 | return accessToken;
243 | }
244 | catch (error) {
245 | // If a response error was encountered...
246 | if (error instanceof ResponseError) {
247 | // If the old refresh token was used, chances are it expired or was invalidated, so...
248 | if (!retrieveNewRefreshToken) {
249 | // Retry with a new refresh token.
250 | logger.log1("Unable to get a new access token, retrying with a new refresh token first.");
251 | return await this.getNewAccessToken(true);
252 | }
253 | // If the new refresh token was used...
254 | else {
255 | // Propagate the error.
256 | throw error;
257 | }
258 | }
259 | // If the root reason is different...
260 | else {
261 | // Propagate the error.
262 | throw error;
263 | }
264 | }
265 | }
266 |
267 | async getAuthenticatedUser() { // https://developers.google.com/people/api/rest/v1/people/get
268 | // Get a new access token.
269 | let accessToken = await this.getNewAccessToken();
270 | // Prepare the authenticated user request URL and data.
271 | let authenticatedUserRequestURL = SERVICE_ENDPOINT + "/v1/people/me";
272 | authenticatedUserRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
273 | personFields: "names,emailAddresses",
274 | access_token: accessToken,
275 | });
276 | let authenticatedUserRequestData = null;
277 | // Perform the request and retrieve the response data.
278 | let responseData = await this.getResponseData("GET", authenticatedUserRequestURL, authenticatedUserRequestData);
279 | // Retrieve the authenticated user.
280 | let authenticatedUser = responseData;
281 | //
282 | logger.log1("PeopleAPI.getAuthenticatedUser(): authenticatedUser = " + JSON.stringify(authenticatedUser));
283 | return authenticatedUser;
284 | }
285 |
286 | /* Contacts. */
287 |
288 | async getContacts() { // https://developers.google.com/people/api/rest/v1/people.connections/list
289 | // Get a new access token.
290 | let accessToken = await this.getNewAccessToken();
291 | // Retrieve the contacts page by page.
292 | let contacts = [];
293 | let nextPageToken = null;
294 | while (true) {
295 | logger.log1("PeopleAPI.getContacts(): nextPageToken = " + nextPageToken);
296 | // Prepare the partial contact request URL and data.
297 | let partialContactRequestURL = SERVICE_ENDPOINT + "/v1/people/me/connections";
298 | partialContactRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
299 | personFields: CONTACT_PERSON_FIELDS,
300 | pageSize: CONTACT_PAGE_SIZE,
301 | sortOrder: "LAST_NAME_ASCENDING",
302 | access_token: accessToken,
303 | });
304 | if (null != nextPageToken) {
305 | partialContactRequestURL += "&pageToken=" + encodeURIComponent(nextPageToken);
306 | }
307 | let partialContactRequestData = null;
308 | // Perform the request and retrieve the response data.
309 | let responseData = await this.getResponseData("GET", partialContactRequestURL, partialContactRequestData);
310 | // Retrieve the partial contacts.
311 | let partialContacts = responseData.connections;
312 | // Concatenate the partial contacts with the contacts.
313 | if (null != partialContacts) {
314 | contacts = contacts.concat(partialContacts);
315 | }
316 | // Retrieve the next page token, necessary to retrieve the next page.
317 | nextPageToken = responseData.nextPageToken;
318 | // Check if this was the last page.
319 | if (null == nextPageToken) {
320 | break;
321 | }
322 | }
323 | //
324 | logger.log1("PeopleAPI.getContacts(): contacts = " + JSON.stringify(contacts));
325 | return contacts;
326 | }
327 |
328 | async createContact(contact) { // https://developers.google.com/people/api/rest/v1/people/createContact
329 | if (null == contact) {
330 | throw new IllegalArgumentError("Invalid 'contact': null.");
331 | }
332 | // Get a new access token.
333 | let accessToken = await this.getNewAccessToken();
334 | // Prepare the contact creation request URL and data.
335 | let contactCreationRequestURL = SERVICE_ENDPOINT + "/v1/people:createContact";
336 | contactCreationRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
337 | personFields: CONTACT_PERSON_FIELDS,
338 | access_token: accessToken,
339 | });
340 | let contactCreationRequestData = contact;
341 | // Perform the request and retrieve the response data.
342 | let responseData = await this.getResponseData("POST", contactCreationRequestURL, contactCreationRequestData);
343 | // Retrieve the response contact.
344 | let responseContact = responseData;
345 | //
346 | logger.log1("PeopleAPI.createContact(): contact = " + JSON.stringify(responseContact));
347 | return responseContact;
348 | }
349 |
350 | async updateContact(contact) { // https://developers.google.com/people/api/rest/v1/people/updateContact
351 | if (null == contact) {
352 | throw new IllegalArgumentError("Invalid 'contact': null.");
353 | }
354 | // Get a new access token.
355 | let accessToken = await this.getNewAccessToken();
356 | // Get the resource name.
357 | let resourceName = contact.resourceName;
358 | // Prepare the contact update request URL and data.
359 | let contactUpdateRequestURL = SERVICE_ENDPOINT + "/v1/" + resourceName + ":updateContact";
360 | contactUpdateRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
361 | updatePersonFields: CONTACT_UPDATE_PERSON_FIELDS,
362 | personFields: CONTACT_PERSON_FIELDS,
363 | access_token: accessToken,
364 | });
365 | let contactUpdateRequestData = contact;
366 | // Perform the request and retrieve the response data.
367 | let responseData = await this.getResponseData("PATCH", contactUpdateRequestURL, contactUpdateRequestData);
368 | // Retrieve the response contact.
369 | let responseContact = responseData;
370 | //
371 | logger.log1("PeopleAPI.updateContact(): contact = " + JSON.stringify(responseContact));
372 | return responseContact;
373 | }
374 |
375 | async deleteContact(resourceName) { // https://developers.google.com/people/api/rest/v1/people/deleteContact
376 | if (null == resourceName) {
377 | throw new IllegalArgumentError("Invalid 'resourceName': null.");
378 | }
379 | // Get a new access token.
380 | let accessToken = await this.getNewAccessToken();
381 | // Prepare the contact deletion request URL and data.
382 | let contactDeletionRequestURL = SERVICE_ENDPOINT + "/v1/" + resourceName + ":deleteContact";
383 | contactDeletionRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
384 | access_token: accessToken,
385 | });
386 | let contactDeletionRequestData = null;
387 | // Perform the request and retrieve the response data.
388 | let responseData = await this.getResponseData("DELETE", contactDeletionRequestURL, contactDeletionRequestData);
389 | //
390 | logger.log1("PeopleAPI.deleteContact(): contact " + resourceName + " deleted.");
391 | return true;
392 | }
393 |
394 | /* Contact groups. */
395 |
396 | async getContactGroups() { // https://developers.google.com/people/api/rest/v1/contactGroups/list
397 | // Get a new access token.
398 | let accessToken = await this.getNewAccessToken();
399 | // Retrieve the contact groups page by page.
400 | let contactGroups = [];
401 | let nextPageToken = null;
402 | while (true) {
403 | logger.log1("PeopleAPI.getContactGroups(): nextPageToken = " + nextPageToken);
404 | // Prepare the partial contact group request URL and data.
405 | let partialContactGroupRequestURL = SERVICE_ENDPOINT + "/v1/contactGroups";
406 | partialContactGroupRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
407 | groupFields: CONTACT_GROUP_FIELDS,
408 | pageSize: CONTACT_GROUP_PAGE_SIZE,
409 | access_token: accessToken,
410 | });
411 | if (null != nextPageToken) {
412 | partialContactGroupRequestURL += "&pageToken=" + encodeURIComponent(nextPageToken);
413 | }
414 | let partialContactGroupRequestData = null;
415 | // Perform the request and retrieve the response data.
416 | let responseData = await this.getResponseData("GET", partialContactGroupRequestURL, partialContactGroupRequestData);
417 | // Retrieve the partial contact groups.
418 | let partialContactGroups = responseData.contactGroups;
419 | // Concatenate the partial contact groups with the contact groups.
420 | if (null != partialContactGroups) {
421 | contactGroups = contactGroups.concat(partialContactGroups);
422 | }
423 | // Retrieve the next page token, necessary to retrieve the next page.
424 | nextPageToken = responseData.nextPageToken;
425 | // Check if this was the last page.
426 | if (null == nextPageToken) {
427 | break;
428 | }
429 | }
430 | //
431 | logger.log1("PeopleAPI.getContactGroups(): contactGroups = " + JSON.stringify(contactGroups));
432 | return contactGroups;
433 | }
434 |
435 | async createContactGroup(contactGroup) { // https://developers.google.com/people/api/rest/v1/contactGroups/create
436 | if (null == contactGroup) {
437 | throw new IllegalArgumentError("Invalid 'contactGroup': null.");
438 | }
439 | // Get a new access token.
440 | let accessToken = await this.getNewAccessToken();
441 | // Prepare the contact group creation request URL and data.
442 | let contactGroupCreationRequestURL = SERVICE_ENDPOINT + "/v1/contactGroups";
443 | contactGroupCreationRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
444 | access_token: accessToken,
445 | });
446 | let contactGroupCreationRequestData = {
447 | "contactGroup": contactGroup,
448 | "readGroupFields": CONTACT_GROUP_FIELDS,
449 | };
450 | // Perform the request and retrieve the response data.
451 | let responseData = await this.getResponseData("POST", contactGroupCreationRequestURL, contactGroupCreationRequestData);
452 | // Retrieve the response contact group.
453 | let responseContactGroup = responseData;
454 | //
455 | logger.log1("PeopleAPI.createContactGroup(): contactGroup = " + JSON.stringify(responseContactGroup));
456 | return responseContactGroup;
457 | }
458 |
459 | async updateContactGroup(contactGroup) { // https://developers.google.com/people/api/rest/v1/contactGroups/update
460 | if (null == contactGroup) {
461 | throw new IllegalArgumentError("Invalid 'contactGroup': null.");
462 | }
463 | // Get a new access token.
464 | let accessToken = await this.getNewAccessToken();
465 | // Get the resource name.
466 | let resourceName = contactGroup.resourceName;
467 | // Prepare the contact group update request URL and data.
468 | let contactGroupUpdateRequestURL = SERVICE_ENDPOINT + "/v1/" + resourceName;
469 | contactGroupUpdateRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
470 | access_token: accessToken,
471 | });
472 | let contactGroupUpdateRequestData = {
473 | "contactGroup": contactGroup,
474 | "updateGroupFields": CONTACT_GROUP_FIELDS,
475 | "readGroupFields": CONTACT_GROUP_FIELDS,
476 | };
477 | // Perform the request and retrieve the response data.
478 | let responseData = await this.getResponseData("PUT", contactGroupUpdateRequestURL, contactGroupUpdateRequestData);
479 | // Retrieve the response contact group.
480 | let responseContactGroup = responseData;
481 | //
482 | logger.log1("PeopleAPI.updateContactGroup(): contactGroup = " + JSON.stringify(responseContactGroup));
483 | return responseContactGroup;
484 | }
485 |
486 | async deleteContactGroup(resourceName) { // https://developers.google.com/people/api/rest/v1/contactGroups/delete
487 | if (null == resourceName) {
488 | throw new IllegalArgumentError("Invalid 'resourceName': null.");
489 | }
490 | // Get a new access token.
491 | let accessToken = await this.getNewAccessToken();
492 | // Prepare the contact group deletion request URL and data.
493 | let contactGroupDeletionRequestURL = SERVICE_ENDPOINT + "/v1/" + resourceName;
494 | contactGroupDeletionRequestURL += "?" + PeopleAPI.getObjectAsEncodedURIParameters({
495 | access_token: accessToken,
496 | });
497 | let contactGroupDeletionRequestData = null;
498 | // Perform the request and retrieve the response data.
499 | let responseData = await this.getResponseData("DELETE", contactGroupDeletionRequestURL, contactGroupDeletionRequestData);
500 | //
501 | logger.log1("PeopleAPI.deleteContactGroup(): contact group " + resourceName + " deleted.");
502 | return true;
503 | }
504 |
505 | /* Connection tests. */
506 |
507 | checkConnection() {
508 | (async () => {
509 | // Attempt the connection.
510 | try {
511 | let authenticatedUser = await this.getAuthenticatedUser();
512 | let authenticatedUserName = authenticatedUser.names[0].displayName;
513 | let authenticatedUserEmail = authenticatedUser.emailAddresses[0].value;
514 | //
515 | let contacts = await this.getContacts();
516 | let contactGroups = await this.getContactGroups();
517 | //
518 | let systemContactGroupCount = 0;
519 | for (let contactGroup of contactGroups) {
520 | if ("SYSTEM_CONTACT_GROUP" === contactGroup.groupType) {
521 | systemContactGroupCount++;
522 | }
523 | }
524 | //
525 | alert("Hi " + authenticatedUserName + " (" + authenticatedUserEmail + ").\nYou have " + contacts.length + " contacts and " + (contactGroups.length - (this.getIncludeSystemContactGroups() ? 0 : systemContactGroupCount)) + " contact groups.");
526 | }
527 | catch (error) {
528 | // If a network error was encountered...
529 | if (error instanceof NetworkError) {
530 | logger.log1("PeopleAPI.checkConnection(): Network error.");
531 | // Alert the user.
532 | alert("Network error, connection aborted!");
533 | }
534 | // If the root reason is different...
535 | else {
536 | // Propagate the error.
537 | throw error;
538 | }
539 | }
540 | })();
541 | }
542 |
543 | /* Helpers. */
544 |
545 | static getObjectAsEncodedURIParameters(x) {
546 | let parameters = [];
547 | //
548 | for (let p in x) {
549 | if (x.hasOwnProperty(p)) {
550 | parameters.push(encodeURIComponent(p) + "=" + encodeURIComponent(x[p]));
551 | }
552 | }
553 | //
554 | return parameters.join("&");
555 | }
556 |
557 | }
558 |
--------------------------------------------------------------------------------
/content/includes/AddressBookSynchronizer.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Google-4-TbSync.
3 | * See CONTRIBUTORS.md for details.
4 | *
5 | * This Source Code Form is subject to the terms of the Mozilla Public
6 | * License, v. 2.0. If a copy of the MPL was not distributed with this
7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 | */
9 |
10 | "use strict";
11 |
12 | if ("undefined" === typeof IllegalArgumentError) {
13 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/IllegalArgumentError.js", this, "UTF-8");
14 | }
15 | if ("undefined" === typeof Logger) {
16 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/Logger.js", this, "UTF-8");
17 | }
18 | if ("undefined" === typeof NetworkError) {
19 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/NetworkError.js", this, "UTF-8");
20 | }
21 | if ("undefined" === typeof PeopleAPI) {
22 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/PeopleAPI.js", this, "UTF-8");
23 | }
24 | if ("undefined" === typeof ResponseError) {
25 | Services.scriptloader.loadSubScript("chrome://google-4-tbsync/content/includes/ResponseError.js", this, "UTF-8");
26 | }
27 |
28 | ChromeUtils.defineESModuleGetters(this, {
29 | VCardPropertyEntry: "resource:///modules/VCardUtils.sys.mjs",
30 | });
31 |
32 | const FAKE_EMAIL_ADDRESS_DOMAIN = "bug1522453.thunderbird.example.com";
33 |
34 | class AddressBookSynchronizer {
35 |
36 | /* Main synchronization. */
37 |
38 | static async synchronize(syncData) {
39 | if (null == syncData) {
40 | throw new IllegalArgumentError("Invalid 'syncData': null.");
41 | }
42 | // Retrieve the target address book.
43 | let targetAddressBook = syncData.target;
44 | if (null == targetAddressBook) {
45 | throw new IllegalArgumentError("Invalid target address book: null.");
46 | }
47 | // Create a new PeopleAPI object.
48 | let peopleAPI = new PeopleAPI(syncData.accountData);
49 | // Retrieve other account properties.
50 | let useFakeEmailAddresses = syncData.accountData.getAccountProperty("useFakeEmailAddresses");
51 | let readOnlyMode = syncData.accountData.getAccountProperty("readOnlyMode");
52 | let verboseLogging = syncData.accountData.getAccountProperty("verboseLogging");
53 | // Enable the logger.
54 | logger = new Logger(verboseLogging);
55 | // Check for the read-only mode.
56 | if (readOnlyMode) {
57 | logger.log0("AddressBookSynchronizer.synchronize(): Read-only mode detected.");
58 | }
59 | // Prepare the variables for the cycles.
60 | logger.log0("AddressBookSynchronizer.synchronize(): Preparing the target address book item map.");
61 | let targetAddressBookItemMap = new Map();
62 | let i = 0;
63 | for (let targetAddressBookItem of await targetAddressBook.getAllItems()) {
64 | let key = targetAddressBookItem.getProperty("X-GOOGLE-RESOURCENAME");
65 | if (("" == key) || (null == key)) {
66 | key = "_local_" + i;
67 | i++;
68 | }
69 | //
70 | targetAddressBookItemMap.set(key, targetAddressBookItem);
71 | }
72 | logger.log0("AddressBookSynchronizer.synchronize(): Preparing the contact group member map.");
73 | let contactGroupMemberMap = new Map();
74 | logger.log0("AddressBookSynchronizer.synchronize(): Retrieving all the local changes since the last synchronization.");
75 | let addedLocalItemIds = targetAddressBook.getAddedItemsFromChangeLog();
76 | let modifiedLocalItemIds = targetAddressBook.getModifiedItemsFromChangeLog();
77 | let deletedLocalItemIds = targetAddressBook.getDeletedItemsFromChangeLog();
78 | // Attempt the synchronization.
79 | try {
80 | logger.log0("AddressBookSynchronizer.synchronize(): Synchronization started.");
81 | // Synchronize contacts.
82 | await AddressBookSynchronizer.synchronizeContacts(peopleAPI, targetAddressBook, targetAddressBookItemMap, contactGroupMemberMap, addedLocalItemIds, modifiedLocalItemIds, deletedLocalItemIds, useFakeEmailAddresses, readOnlyMode);
83 | // Synchronize contact groups.
84 | await AddressBookSynchronizer.synchronizeContactGroups(peopleAPI, targetAddressBook, targetAddressBookItemMap, addedLocalItemIds, modifiedLocalItemIds, deletedLocalItemIds, readOnlyMode);
85 | // Synchronize contact group members.
86 | await AddressBookSynchronizer.synchronizeContactGroupMembers(targetAddressBook, targetAddressBookItemMap, contactGroupMemberMap, readOnlyMode);
87 | // Fix the change log.
88 | await AddressBookSynchronizer.fixChangeLog(targetAddressBook, targetAddressBookItemMap);
89 | //
90 | logger.log0("AddressBookSynchronizer.synchronize(): Synchronization finished.");
91 | }
92 | catch (error) {
93 | // If a network error was encountered...
94 | if (error instanceof NetworkError) {
95 | logger.log0("AddressBookSynchronizer.synchronize(): Network error.");
96 | // Propagate the error.
97 | throw error;
98 | }
99 | // If the root reason is different...
100 | else {
101 | // Propagate the error.
102 | throw error;
103 | }
104 | }
105 | }
106 |
107 | /* Contacts. */
108 |
109 | static async synchronizeContacts(peopleAPI, targetAddressBook, targetAddressBookItemMap, contactGroupMemberMap, addedLocalItemIds, modifiedLocalItemIds, deletedLocalItemIds, useFakeEmailAddresses, readOnlyMode) {
110 | if (null == peopleAPI) {
111 | throw new IllegalArgumentError("Invalid 'peopleAPI': null.");
112 | }
113 | if (null == targetAddressBook) {
114 | throw new IllegalArgumentError("Invalid 'targetAddressBook': null.");
115 | }
116 | if (null == targetAddressBookItemMap) {
117 | throw new IllegalArgumentError("Invalid 'targetAddressBookItemMap': null.");
118 | }
119 | if (null == contactGroupMemberMap) {
120 | throw new IllegalArgumentError("Invalid 'contactGroupMemberMap': null.");
121 | }
122 | if (null == addedLocalItemIds) {
123 | throw new IllegalArgumentError("Invalid 'addedLocalItemIds': null.");
124 | }
125 | if (null == modifiedLocalItemIds) {
126 | throw new IllegalArgumentError("Invalid 'modifiedLocalItemIds': null.");
127 | }
128 | if (null == deletedLocalItemIds) {
129 | throw new IllegalArgumentError("Invalid 'deletedLocalItemIds': null.");
130 | }
131 | if (null == useFakeEmailAddresses) {
132 | throw new IllegalArgumentError("Invalid 'useFakeEmailAddresses': null.");
133 | }
134 | if (null == readOnlyMode) {
135 | throw new IllegalArgumentError("Invalid 'readOnlyMode': null.");
136 | }
137 | // Retrieve all server contacts.
138 | let serverContacts = await peopleAPI.getContacts();
139 | // Cycle on the server contacts.
140 | logger.log0("AddressBookSynchronizer.synchronizeContacts(): Cycling on the server contacts.");
141 | for (let serverContact of serverContacts) {
142 | // Get the resource name (in the form 'people/personId') and the display name.
143 | let resourceName = serverContact.resourceName;
144 | let displayName = (serverContact.names ? serverContact.names[0].displayName : "-");
145 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ")");
146 | // Try to match the server contact locally.
147 | let localContact = targetAddressBookItemMap.get(resourceName);
148 | // If such a local contact is currently unavailable...
149 | if (undefined === localContact) {
150 | // ...and if it was previously deleted locally...
151 | if (deletedLocalItemIds.includes(resourceName)) {
152 | // Check we are not in read-only mode, then...
153 | if (!readOnlyMode) {
154 | // Delete the server contact remotely.
155 | await peopleAPI.deleteContact(resourceName);
156 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ") has been deleted remotely.");
157 | }
158 | // Remove the resource name from the local change log (deleted items).
159 | await targetAddressBook.removeItemFromChangeLog(resourceName);
160 | }
161 | // ...and if it wasn't previously deleted locally...
162 | else {
163 | // Create a new local contact.
164 | localContact = targetAddressBook.createNewCard();
165 | // Import the server contact information into the local contact.
166 | localContact.setProperty("X-GOOGLE-RESOURCENAME", resourceName);
167 | localContact.setProperty("X-GOOGLE-ETAG", serverContact.etag);
168 | localContact = AddressBookSynchronizer.fillLocalContactWithServerContactInformation(localContact, serverContact, useFakeEmailAddresses);
169 | // Add the local contact locally, keep the target address book item map up-to-date.
170 | await targetAddressBook.addItem(localContact, true);
171 | targetAddressBookItemMap.set(resourceName, localContact);
172 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ") has been added locally.");
173 | // Remove the resource name from the local change log (added items).
174 | // (This should be logically useless, but sometimes the change log is filled with some of the contacts added above.)
175 | await targetAddressBook.removeItemFromChangeLog(resourceName);
176 | // Update the contact group member map.
177 | AddressBookSynchronizer.updateContactGroupMemberMap(contactGroupMemberMap, resourceName, serverContact.memberships);
178 | }
179 | }
180 | // If such a local contact is currently available...
181 | else {
182 | // ...and if the server one is more recent, or if we are in read-only mode...
183 | if ((localContact.getProperty("X-GOOGLE-ETAG") !== serverContact.etag) || (readOnlyMode)) {
184 | // Import the server contact information into the local contact.
185 | localContact.setProperty("X-GOOGLE-ETAG", serverContact.etag);
186 | localContact = AddressBookSynchronizer.fillLocalContactWithServerContactInformation(localContact, serverContact, useFakeEmailAddresses);
187 | // Update the local contact locally, and keep the target address book item map up-to-date.
188 | await targetAddressBook.modifyItem(localContact, true);
189 | targetAddressBookItemMap.set(resourceName, localContact);
190 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ") has been updated locally.");
191 | // Remove the resource name from the local change log (modified items).
192 | await targetAddressBook.removeItemFromChangeLog(resourceName);
193 | }
194 | // Update the contact group member map.
195 | AddressBookSynchronizer.updateContactGroupMemberMap(contactGroupMemberMap, resourceName, serverContact.memberships);
196 | }
197 | }
198 | // Prepare the variables for the cycles.
199 | let addedLocalItemResourceNames = new Set();
200 | // Cycle on the locally added contacts.
201 | logger.log0("AddressBookSynchronizer.synchronizeContacts(): Cycling on the locally added contacts.");
202 | for (let localContactId of addedLocalItemIds) {
203 | // Retrieve the local contact, and make sure such an item is actually valid and not a contact group.
204 | let localContact = targetAddressBookItemMap.get(localContactId);
205 | if (undefined === localContact) {
206 | continue;
207 | }
208 | if (localContact.isMailList) {
209 | continue;
210 | }
211 | // Check we are not in read-only mode, then...
212 | if (!readOnlyMode) {
213 | // Create a new server contact.
214 | let serverContact = {};
215 | // Import the local contact information into the server contact.
216 | serverContact = AddressBookSynchronizer.fillServerContactWithLocalContactInformation(localContact, serverContact, useFakeEmailAddresses);
217 | // Add the server contact remotely and get the resource name (in the form 'people/personId') and the display name.
218 | serverContact = await peopleAPI.createContact(serverContact);
219 | let resourceName = serverContact.resourceName;
220 | let displayName = (serverContact.names ? serverContact.names[0].displayName : "-");
221 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ") has been added remotely.");
222 | // Import the server contact information into the local contact.
223 | localContact.setProperty("X-GOOGLE-RESOURCENAME", resourceName);
224 | localContact.setProperty("X-GOOGLE-ETAG", serverContact.etag);
225 | localContact = AddressBookSynchronizer.fillLocalContactWithServerContactInformation(localContact, serverContact, useFakeEmailAddresses);
226 | // Update the local contact locally, and keep the target address book item map up-to-date.
227 | await targetAddressBook.modifyItem(localContact, true);
228 | targetAddressBookItemMap.set(resourceName, localContact);
229 | // Add the resource name to the proper set.
230 | addedLocalItemResourceNames.add(resourceName);
231 | }
232 | // Remove the local contact id from the local change log (added items).
233 | await targetAddressBook.removeItemFromChangeLog(localContactId);
234 | }
235 | // Cycle on the locally modified contacts.
236 | logger.log0("AddressBookSynchronizer.synchronizeContacts(): Cycling on the locally modified contacts.");
237 | for (let localContactId of modifiedLocalItemIds) {
238 | // Retrieve the local contact, and make sure such an item is actually valid and not a contact group.
239 | let localContact = targetAddressBookItemMap.get(localContactId);
240 | if (undefined === localContact) {
241 | continue;
242 | }
243 | if (localContact.isMailList) {
244 | continue;
245 | }
246 | // Check we are not in read-only mode, then...
247 | if (!readOnlyMode) {
248 | // Create a new server contact.
249 | let serverContact = {};
250 | serverContact.resourceName = localContact.getProperty("X-GOOGLE-RESOURCENAME");
251 | serverContact.etag = localContact.getProperty("X-GOOGLE-ETAG");
252 | // Import the local contact information into the server contact.
253 | serverContact = AddressBookSynchronizer.fillServerContactWithLocalContactInformation(localContact, serverContact, useFakeEmailAddresses);
254 | // Update the server contact remotely or delete the local contact locally.
255 | try {
256 | // Update the server contact remotely and get the resource name (in the form 'people/personId') and the display name.
257 | serverContact = await peopleAPI.updateContact(serverContact);
258 | let resourceName = serverContact.resourceName;
259 | let displayName = (serverContact.names ? serverContact.names[0].displayName : "-");
260 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ") has been updated remotely.");
261 | // Import the server contact information into the local contact.
262 | localContact.setProperty("X-GOOGLE-RESOURCENAME", resourceName);
263 | localContact.setProperty("X-GOOGLE-ETAG", serverContact.etag);
264 | localContact = AddressBookSynchronizer.fillLocalContactWithServerContactInformation(localContact, serverContact, useFakeEmailAddresses);
265 | // Update the local contact locally, and keep the target address book item map up-to-date.
266 | await targetAddressBook.modifyItem(localContact, true);
267 | targetAddressBookItemMap.set(resourceName, localContact);
268 | }
269 | catch (error) {
270 | // If the server contact is no longer available (i.e.: it was deleted)...
271 | if ((error instanceof ResponseError) && (error.message.includes("404"))) {
272 | // Get the resource name (in the form 'people/personId').
273 | let resourceName = localContact.getProperty("X-GOOGLE-RESOURCENAME");
274 | let displayName = (localContact.getProperty("DisplayName") ? localContact.getProperty("DisplayName") : "-");
275 | // Delete the local contact locally, and keep the target address book item map up-to-date.
276 | targetAddressBook.deleteItem(localContact, true);
277 | targetAddressBookItemMap.delete(resourceName);
278 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + resourceName + " (" + displayName + ") has been deleted locally.");
279 | }
280 | // If the root reason is different...
281 | else {
282 | // Propagate the error.
283 | throw error;
284 | }
285 | }
286 | }
287 | // Remove the local contact id from the local change log (modified items).
288 | await targetAddressBook.removeItemFromChangeLog(localContactId);
289 | }
290 | // Determine all the contacts which were previously deleted remotely and delete them locally.
291 | logger.log0("AddressBookSynchronizer.synchronizeContacts(): Determining all the remotely deleted contacts.");
292 | for (let localContact of targetAddressBookItemMap.values()) {
293 | // Make sure the item is actually valid and not a contact group.
294 | if (undefined === targetAddressBookItemMap.get(localContact.getProperty("X-GOOGLE-RESOURCENAME"))) {
295 | continue;
296 | }
297 | if (localContact.isMailList) {
298 | continue;
299 | }
300 | // Get the local contact id and the display name.
301 | let localContactId = localContact.getProperty("X-GOOGLE-RESOURCENAME");
302 | let displayName = localContact.getProperty("DisplayName");
303 | // Check if the local contact id matches any of the locally added contacts.
304 | if (addedLocalItemResourceNames.has(localContactId)) {
305 | continue;
306 | }
307 | // Check if the local contact id matches any of the resource names downloaded.
308 | let localContactFoundAmongServerContacts = false;
309 | for (let serverContact of serverContacts) {
310 | if (localContactId === serverContact.resourceName) {
311 | localContactFoundAmongServerContacts = true;
312 | break;
313 | }
314 | }
315 | if (localContactFoundAmongServerContacts) {
316 | continue;
317 | }
318 | // Delete the local contact locally, and keep the target address book item map up-to-date.
319 | targetAddressBook.deleteItem(localContact, true);
320 | targetAddressBookItemMap.delete(localContactId);
321 | logger.log1("AddressBookSynchronizer.synchronizeContacts(): " + localContactId + " (" + displayName + ") has been deleted locally.");
322 | }
323 | }
324 |
325 | static fillLocalContactWithServerContactInformation(localContact, serverContact, useFakeEmailAddresses) {
326 | if (null == localContact) {
327 | throw new IllegalArgumentError("Invalid 'localContact': null.");
328 | }
329 | if (null == serverContact) {
330 | throw new IllegalArgumentError("Invalid 'serverContact': null.");
331 | }
332 | if (null == useFakeEmailAddresses) {
333 | throw new IllegalArgumentError("Invalid 'useFakeEmailAddresses': null.");
334 | }
335 | // Reset all the properties managed by this method.
336 | localContact._card.vCardProperties.clearValues("n");
337 | localContact._card.vCardProperties.clearValues("fn");
338 | localContact._card.vCardProperties.clearValues("nickname");
339 | localContact._card.vCardProperties.clearValues("email");
340 | localContact._card.vCardProperties.clearValues("url");
341 | localContact._card.vCardProperties.clearValues("adr");
342 | localContact._card.vCardProperties.clearValues("tel");
343 | localContact._card.vCardProperties.clearValues("impp");
344 | localContact._card.vCardProperties.clearValues("bday");
345 | localContact._card.vCardProperties.clearValues("anniversary");
346 | localContact._card.vCardProperties.clearValues("note");
347 | localContact._card.vCardProperties.clearValues("title");
348 | localContact._card.vCardProperties.clearValues("org");
349 | localContact._card.vCardProperties.clearValues("x-custom1");
350 | localContact._card.vCardProperties.clearValues("x-custom2");
351 | localContact._card.vCardProperties.clearValues("x-custom3");
352 | localContact._card.vCardProperties.clearValues("x-custom4");
353 | // Set the name and the display name.
354 | if (serverContact.names) {
355 | let name_found = false;
356 | let name = null;
357 | for (name of serverContact.names) {
358 | if ("CONTACT" === name.metadata.source.type) {
359 | name_found = true;
360 | break;
361 | }
362 | }
363 | //
364 | if (name_found) {
365 | let n_values = [ "", "", "", "", "" ];
366 | let fn_values = [ "" ];
367 | //
368 | if (name.honorificPrefix) {
369 | n_values[3] = name.honorificPrefix.replaceAll(", ", " ").replaceAll(",", " ");
370 | }
371 | if (name.givenName) {
372 | n_values[1] = name.givenName.replaceAll(", ", " ").replaceAll(",", " ");
373 | }
374 | if (name.middleName) {
375 | n_values[2] = name.middleName.replaceAll(", ", " ").replaceAll(",", " ");
376 | }
377 | if (name.familyName) {
378 | n_values[0] = name.familyName.replaceAll(", ", " ").replaceAll(",", " ");
379 | }
380 | if (name.honorificSuffix) {
381 | n_values[4] = name.honorificSuffix.replaceAll(", ", " ").replaceAll(",", " ");
382 | }
383 | if (name.displayName) {
384 | fn_values[0] = name.displayName.replaceAll(", ", " ").replaceAll(",", " ");
385 | }
386 | //
387 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("n", {}, "array", n_values));
388 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("fn", {}, "text", fn_values[0]));
389 | }
390 | }
391 | // Set the nickname.
392 | if (serverContact.nicknames) {
393 | let nickname_found = false;
394 | let nickname = serverContact.nicknames[0];
395 | nickname_found = true;
396 | //
397 | if (nickname_found) {
398 | let nickname_values = [ "" ];
399 | //
400 | if (nickname.value) {
401 | nickname_values[0] = nickname.value.replaceAll(", ", " ").replaceAll(",", " ");
402 | }
403 | //
404 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("nickname", {}, "array", nickname_values));
405 | }
406 | }
407 | // Set the email addresses.
408 | if (serverContact.emailAddresses) {
409 | let pref_param = "1";
410 | //
411 | for (let emailAddress of serverContact.emailAddresses) {
412 | if ("CONTACT" !== emailAddress.metadata.source.type) {
413 | continue;
414 | }
415 | //
416 | let email_values = [ "" ];
417 | let email_type_param = "";
418 | //
419 | if (emailAddress.value) {
420 | email_values[0] = emailAddress.value.replaceAll(", ", " ").replaceAll(",", " ");
421 | }
422 | switch (emailAddress.type) {
423 | case "home":
424 | email_type_param = "home";
425 | //
426 | break;
427 | case "work":
428 | email_type_param = "work";
429 | //
430 | break;
431 | default:
432 | email_type_param = "";
433 | //
434 | break;
435 | }
436 | //
437 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("email", { type: email_type_param, pref: pref_param }, "text", email_values[0]));
438 | //
439 | pref_param = "";
440 | }
441 | }
442 | else if (useFakeEmailAddresses) {
443 | let email_values = [ "" ];
444 | let email_type_param = "";
445 | //
446 | email_values[0] = Date.now() + "." + Math.random() + "@" + FAKE_EMAIL_ADDRESS_DOMAIN;
447 | //
448 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("email", { type: email_type_param }, "text", email_values[0]));
449 | }
450 | // Set the websites.
451 | if (serverContact.urls) {
452 | for (let url of serverContact.urls) {
453 | let url_values = [ "" ];
454 | let url_type_param = "";
455 | //
456 | if (url.value) {
457 | url_values[0] = url.value.replaceAll(", ", " ").replaceAll(",", " ");
458 | }
459 | switch (url.type) {
460 | case "work":
461 | url_type_param = "work";
462 | //
463 | break;
464 | default:
465 | url_type_param = "";
466 | //
467 | break;
468 | }
469 | //
470 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("url", { type: url_type_param }, "array", url_values));
471 | }
472 | }
473 | // Set the addresses.
474 | if (serverContact.addresses) {
475 | for (let address of serverContact.addresses) {
476 | let adr_values = [ "", "", "", "", "", "", "" ];
477 | let adr_type_param = "";
478 | //
479 | if (address.streetAddress || address.extendedAddress) {
480 | adr_values[2] = [];
481 | }
482 | if (address.streetAddress) {
483 | adr_values[2][0] = address.streetAddress.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
484 | }
485 | if (address.extendedAddress) {
486 | if (!address.streetAddress) {
487 | adr_values[2][0] = "-";
488 | }
489 | adr_values[2][1] = address.extendedAddress.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
490 | }
491 | if (address.city) {
492 | adr_values[3] = address.city.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
493 | }
494 | if (address.region) {
495 | adr_values[4] = address.region.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
496 | }
497 | if (address.postalCode) {
498 | adr_values[5] = address.postalCode.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
499 | }
500 | if (address.country) {
501 | adr_values[6] = address.country.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
502 | }
503 | switch (address.type) {
504 | case "home":
505 | adr_type_param = "home";
506 | //
507 | break;
508 | case "work":
509 | adr_type_param = "work";
510 | //
511 | break;
512 | default:
513 | adr_type_param = "";
514 | //
515 | break;
516 | }
517 | //
518 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("adr", { type: adr_type_param }, "array", adr_values));
519 | }
520 | }
521 | // Set the phone numbers.
522 | if (serverContact.phoneNumbers) {
523 | for (let phoneNumber of serverContact.phoneNumbers) {
524 | let tel_values = [ "" ];
525 | let tel_type_param = "";
526 | //
527 | if (phoneNumber.value) {
528 | tel_values[0] = phoneNumber.value.replaceAll(", ", " ").replaceAll(",", " ");
529 | }
530 | switch (phoneNumber.type) {
531 | case "home":
532 | tel_type_param = "home";
533 | //
534 | break;
535 | case "work":
536 | tel_type_param = "work";
537 | //
538 | break;
539 | case "mobile":
540 | tel_type_param = "cell";
541 | //
542 | break;
543 | case "homeFax":
544 | case "workFax":
545 | tel_type_param = "fax";
546 | //
547 | break;
548 | case "pager":
549 | tel_type_param = "pager";
550 | //
551 | break;
552 | default:
553 | tel_type_param = "";
554 | //
555 | break;
556 | }
557 | //
558 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("tel", { type: tel_type_param }, "array", tel_values));
559 | }
560 | }
561 | // Set the chat accounts.
562 | if (serverContact.imClients) {
563 | for (let imClient of serverContact.imClients) {
564 | let impp_values = [ "" ];
565 | //
566 | if (imClient.protocol && imClient.username) {
567 | impp_values[0] = imClient.protocol.replaceAll(", ", " ").replaceAll(",", " ") + ":" + imClient.username.replaceAll(", ", " ").replaceAll(",", " ");
568 | }
569 | //
570 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("impp", {}, "array", impp_values));
571 | }
572 | }
573 | // Set the special dates.
574 | if (serverContact.birthdays) {
575 | let birthday_found = false;
576 | let birthday = serverContact.birthdays[0];
577 | birthday_found = true;
578 | //
579 | if (birthday_found) {
580 | let bday_values = [ "" ];
581 | //
582 | if (birthday.date) {
583 | let year = (birthday.date.year ? String(birthday.date.year).padStart(4, "0") : "-");
584 | let month = (birthday.date.month ? String(birthday.date.month).padStart(2, "0") : "-");
585 | let day = (birthday.date.day ? String(birthday.date.day).padStart(2, "0") : "-");
586 | //
587 | bday_values[0] = year + "-" + month + "-" + day;
588 | }
589 | //
590 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("bday", {}, "date-and-or-time", bday_values[0]));
591 | }
592 | }
593 | if (serverContact.events) {
594 | for (let event of serverContact.events) {
595 | let anniversary_values = [ "" ];
596 | //
597 | if (event && event.date) {
598 | let year = (event.date.year ? String(event.date.year).padStart(4, "0") : "-");
599 | let month = (event.date.month ? String(event.date.month).padStart(2, "0") : "-");
600 | let day = (event.date.day ? String(event.date.day).padStart(2, "0") : "-");
601 | //
602 | anniversary_values[0] = year + "-" + month + "-" + day;
603 | }
604 | //
605 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("anniversary", {}, "date-and-or-time", anniversary_values[0]));
606 | }
607 | }
608 | // Set the notes.
609 | if (serverContact.biographies) {
610 | let note_values = [ "" ];
611 | //
612 | if (serverContact.biographies[0] && serverContact.biographies[0].value) {
613 | note_values[0] = serverContact.biographies[0].value.replaceAll(", ", " ").replaceAll(",", " ").replace(/(\r\n|\n|\r)/gm, " - ");
614 | }
615 | //
616 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("note", {}, "array", note_values));
617 | }
618 | // Set the organizational properties.
619 | if (serverContact.organizations) {
620 | let organization_found = false;
621 | let organization = serverContact.organizations[0];
622 | organization_found = true;
623 | //
624 | if (organization_found) {
625 | let title_values = [ "" ];
626 | let org_values = [ "" ];
627 | //
628 | if (organization.title) {
629 | title_values[0] = organization.title.replaceAll(", ", " ").replaceAll(",", " ");
630 | }
631 | if (organization.name) {
632 | org_values[0] = organization.name.replaceAll(", ", " ").replaceAll(",", " ");
633 | }
634 | if (organization.department) {
635 | // FIXME: temporary.
636 | if (!organization.name) {
637 | org_values[0] = "-"; // necessary because TB considers the first item to be the name
638 | }
639 | org_values[1] = organization.department.replaceAll(", ", " ").replaceAll(",", " ");
640 | }
641 | //
642 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("title", {}, "array", title_values));
643 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("org", {}, "array", org_values));
644 | }
645 | }
646 | // Set the custom properties.
647 | if (serverContact.userDefined) {
648 | let x_custom1_values = [ "" ];
649 | let x_custom2_values = [ "" ];
650 | let x_custom3_values = [ "" ];
651 | let x_custom4_values = [ "" ];
652 | //
653 | if (serverContact.userDefined[0] && serverContact.userDefined[0].value) {
654 | x_custom1_values[0] = serverContact.userDefined[0].value.replaceAll(", ", " ").replaceAll(",", " ");
655 | }
656 | if (serverContact.userDefined[1] && serverContact.userDefined[1].value) {
657 | x_custom2_values[0] = serverContact.userDefined[1].value.replaceAll(", ", " ").replaceAll(",", " ");
658 | }
659 | if (serverContact.userDefined[2] && serverContact.userDefined[2].value) {
660 | x_custom3_values[0] = serverContact.userDefined[2].value.replaceAll(", ", " ").replaceAll(",", " ");
661 | }
662 | if (serverContact.userDefined[3] && serverContact.userDefined[3].value) {
663 | x_custom4_values[0] = serverContact.userDefined[3].value.replaceAll(", ", " ").replaceAll(",", " ");
664 | }
665 | //
666 | /* FIXME: temporary workaround for a TB bug.
667 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom1", {}, "array", x_custom1_values));
668 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom2", {}, "array", x_custom2_values));
669 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom3", {}, "array", x_custom3_values));
670 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom4", {}, "array", x_custom4_values));
671 | */
672 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom1", {}, "array", x_custom1_values[0]));
673 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom2", {}, "array", x_custom2_values[0]));
674 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom3", {}, "array", x_custom3_values[0]));
675 | localContact._card.vCardProperties.addEntry(new VCardPropertyEntry("x-custom4", {}, "array", x_custom4_values[0]));
676 | }
677 | //
678 | return localContact;
679 | }
680 |
681 | static fillServerContactWithLocalContactInformation(localContact, serverContact, useFakeEmailAddresses) {
682 | if (null == localContact) {
683 | throw new IllegalArgumentError("Invalid 'localContact': null.");
684 | }
685 | if (null == serverContact) {
686 | throw new IllegalArgumentError("Invalid 'serverContact': null.");
687 | }
688 | if (null == useFakeEmailAddresses) {
689 | throw new IllegalArgumentError("Invalid 'useFakeEmailAddresses': null.");
690 | }
691 | // Reset all the properties managed by this method.
692 | delete serverContact.names;
693 | delete serverContact.nicknames;
694 | delete serverContact.emailAddresses;
695 | delete serverContact.urls;
696 | delete serverContact.addresses;
697 | delete serverContact.phoneNumbers;
698 | delete serverContact.imClients;
699 | delete serverContact.birthdays;
700 | delete serverContact.events;
701 | delete serverContact.biographies;
702 | delete serverContact.organizations;
703 | delete serverContact.userDefined;
704 | // Set the name and the display name.
705 | let n_entry = localContact._card.vCardProperties.getFirstEntry("n");
706 | let fn_entry = localContact._card.vCardProperties.getFirstEntry("fn");
707 | if (n_entry || fn_entry) {
708 | serverContact.names = [];
709 | //
710 | serverContact.names[0] = {};
711 | //
712 | if (n_entry) {
713 | let n_values = n_entry.value; // n_entry.value: array
714 | //
715 | if (n_values[3]) {
716 | serverContact.names[0].honorificPrefix = n_values[3];
717 | }
718 | if (n_values[1]) {
719 | serverContact.names[0].givenName = n_values[1];
720 | }
721 | if (n_values[2]) {
722 | serverContact.names[0].middleName = n_values[2];
723 | }
724 | if (n_values[0]) {
725 | serverContact.names[0].familyName = n_values[0];
726 | }
727 | if (n_values[4]) {
728 | serverContact.names[0].honorificSuffix = n_values[4];
729 | }
730 | }
731 | /* Disabled, as names[0].displayName is an output-only field for Google.
732 | if (fn_entry) {
733 | let fn_values = [ fn_entry.value ]; // fn_entry.value: string
734 | //
735 | if (fn_values[0]) {
736 | serverContact.names[0].displayName = fn_values[0];
737 | }
738 | }
739 | */
740 | }
741 | // Set the nickname.
742 | let nickname_entry = localContact._card.vCardProperties.getFirstEntry("nickname");
743 | if (nickname_entry) {
744 | serverContact.nicknames = [];
745 | //
746 | serverContact.nicknames[0] = {};
747 | //
748 | let nickname_values = [ nickname_entry.value ]; // nickname_entry.value: string
749 | //
750 | if (nickname_values[0]) {
751 | serverContact.nicknames[0].value = nickname_values[0];
752 | }
753 | }
754 | // Set the email addresses.
755 | let email_entries = localContact._card.vCardProperties.getAllEntries("email");
756 | if (email_entries) {
757 | serverContact.emailAddresses = [];
758 | let i = 0;
759 | //
760 | for (let email_entry of email_entries) {
761 | if ((email_entry.value.endsWith("@" + FAKE_EMAIL_ADDRESS_DOMAIN)) && (useFakeEmailAddresses)) {
762 | continue;
763 | }
764 | //
765 | serverContact.emailAddresses[i] = {};
766 | //
767 | let email_values = [ email_entry.value ]; // email_entry.value: string
768 | let email_type_param = email_entry.params.type;
769 | //
770 | if (email_values[0]) {
771 | serverContact.emailAddresses[i].value = email_values[0];
772 | }
773 | switch (email_type_param) {
774 | case "work":
775 | serverContact.emailAddresses[i].type = "work";
776 | //
777 | break;
778 | case "home":
779 | serverContact.emailAddresses[i].type = "home";
780 | //
781 | break;
782 | default:
783 | serverContact.emailAddresses[i].type = "other";
784 | //
785 | break;
786 | }
787 | //
788 | i++;
789 | }
790 | //
791 | if (0 == serverContact.emailAddresses.length) {
792 | delete serverContact.emailAddresses;
793 | }
794 | }
795 | // Set the websites.
796 | let url_entries = localContact._card.vCardProperties.getAllEntries("url");
797 | if (url_entries) {
798 | serverContact.urls = [];
799 | let i = 0;
800 | //
801 | for (let url_entry of url_entries) {
802 | serverContact.urls[i] = {};
803 | //
804 | let url_values = [ url_entry.value ]; // url_entry.value: string
805 | let url_type_param = url_entry.params.type;
806 | //
807 | if (url_values[0]) {
808 | serverContact.urls[i].value = url_values[0];
809 | }
810 | switch (url_type_param) {
811 | case "work":
812 | serverContact.urls[i].type = "work";
813 | //
814 | break;
815 | default:
816 | serverContact.urls[i].type = "";
817 | //
818 | break;
819 | }
820 | //
821 | i++;
822 | }
823 | }
824 | // Set the addresses.
825 | let adr_entries = localContact._card.vCardProperties.getAllEntries("adr");
826 | if (adr_entries) {
827 | serverContact.addresses = [];
828 | let i = 0;
829 | //
830 | for (let adr_entry of adr_entries) {
831 | serverContact.addresses[i] = {};
832 | //
833 | let adr_values = adr_entry.value; // adr_entry.value: array
834 | let adr_type_param = adr_entry.params.type;
835 | //
836 | if (adr_values[2]) {
837 | let adr_values_2_values = (Array.isArray(adr_values[2]) ? adr_values[2] : [ adr_values[2] ]); // adr_values[2]: string or array
838 | //
839 | if (adr_values_2_values[0]) {
840 | serverContact.addresses[i].streetAddress = adr_values_2_values[0];
841 | }
842 | if (adr_values_2_values[1]) {
843 | serverContact.addresses[i].extendedAddress = adr_values_2_values[1];
844 | }
845 | }
846 | if (adr_values[3]) {
847 | serverContact.addresses[i].city = adr_values[3];
848 | }
849 | if (adr_values[4]) {
850 | serverContact.addresses[i].region = adr_values[4];
851 | }
852 | if (adr_values[5]) {
853 | serverContact.addresses[i].postalCode = adr_values[5];
854 | }
855 | if (adr_values[6]) {
856 | serverContact.addresses[i].country = adr_values[6];
857 | }
858 | switch (adr_type_param) {
859 | case "work":
860 | serverContact.addresses[i].type = "work";
861 | //
862 | break;
863 | case "home":
864 | serverContact.addresses[i].type = "home";
865 | //
866 | break;
867 | default:
868 | serverContact.addresses[i].type = "";
869 | //
870 | break;
871 | }
872 | //
873 | i++;
874 | }
875 | }
876 | // Set the phone numbers.
877 | let tel_entries = localContact._card.vCardProperties.getAllEntries("tel");
878 | if (tel_entries) {
879 | serverContact.phoneNumbers = [];
880 | let i = 0;
881 | //
882 | for (let tel_entry of tel_entries) {
883 | serverContact.phoneNumbers[i] = {};
884 | //
885 | let tel_values = [ tel_entry.value ]; // tel_entry.value: string
886 | let tel_type_param = tel_entry.params.type;
887 | //
888 | if (tel_values[0]) {
889 | serverContact.phoneNumbers[i].value = tel_values[0];
890 | }
891 | switch (tel_type_param) {
892 | case "work":
893 | serverContact.phoneNumbers[i].type = "work";
894 | //
895 | break;
896 | case "home":
897 | serverContact.phoneNumbers[i].type = "home";
898 | //
899 | break;
900 | case "cell":
901 | serverContact.phoneNumbers[i].type = "mobile";
902 | //
903 | break;
904 | case "fax":
905 | serverContact.phoneNumbers[i].type = "Work Fax";
906 | //
907 | break;
908 | case "pager":
909 | serverContact.phoneNumbers[i].type = "pager";
910 | //
911 | break;
912 | default:
913 | serverContact.phoneNumbers[i].type = "";
914 | //
915 | break;
916 | }
917 | //
918 | i++;
919 | }
920 | }
921 | // Set the chat accounts.
922 | let impp_entries = localContact._card.vCardProperties.getAllEntries("impp");
923 | if (impp_entries) {
924 | serverContact.imClients = [];
925 | let i = 0;
926 | //
927 | for (let impp_entry of impp_entries) {
928 | serverContact.imClients[i] = {};
929 | //
930 | let impp_values = [ impp_entry.value ]; // impp_entry.value: string
931 | //
932 | if (impp_values[0] && impp_values[0].includes(":")) {
933 | let impp_values_pu = impp_values[0].split(":");
934 | //
935 | serverContact.imClients[i].username = impp_values_pu[1];
936 | serverContact.imClients[i].protocol = impp_values_pu[0];
937 | }
938 | //
939 | i++;
940 | }
941 | }
942 | // Set the special dates.
943 | let bday_entry = localContact._card.vCardProperties.getFirstEntry("bday");
944 | let anniversary_entries = localContact._card.vCardProperties.getAllEntries("anniversary");
945 | if (bday_entry) {
946 | serverContact.birthdays = [];
947 | //
948 | serverContact.birthdays[0] = {};
949 | serverContact.birthdays[0].date = {};
950 | //
951 | let bday_values = [ bday_entry.value ]; // bday_entry.value: string
952 | //
953 | let year = "";
954 | let month = "";
955 | let day = "";
956 | let c = 0;
957 | if ("-" == bday_values[0].substring(c, c + 1)) {
958 | c += 2;
959 | }
960 | else {
961 | year = bday_values[0].substring(c, c + 4);
962 | c += 5;
963 | }
964 | if ("-" == bday_values[0].substring(c, c + 1)) {
965 | c += 2;
966 | }
967 | else {
968 | month = bday_values[0].substring(c, c + 2);
969 | c += 3;
970 | }
971 | if ("-" == bday_values[0].substring(c, c + 1)) {
972 | c += 2;
973 | }
974 | else {
975 | day = bday_values[0].substring(c, c + 2);
976 | c += 3;
977 | }
978 | //
979 | if (month) {
980 | serverContact.birthdays[0].date.month = month;
981 | }
982 | if (day) {
983 | serverContact.birthdays[0].date.day = day;
984 | }
985 | if (year) {
986 | serverContact.birthdays[0].date.year = year;
987 | }
988 | }
989 | if (anniversary_entries) {
990 | serverContact.events = [];
991 | let i = 0;
992 | //
993 | for (let anniversary_entry of anniversary_entries) {
994 | serverContact.events[i] = {};
995 | serverContact.events[i].date = {};
996 | //
997 | let anniversary_values = [ anniversary_entry.value ]; // anniversary_entry.value: string
998 | //
999 | let year = "";
1000 | let month = "";
1001 | let day = "";
1002 | let c = 0;
1003 | if ("-" == anniversary_values[0].substring(c, c + 1)) {
1004 | c += 2;
1005 | }
1006 | else {
1007 | year = anniversary_values[0].substring(c, c + 4);
1008 | c += 5;
1009 | }
1010 | if ("-" == anniversary_values[0].substring(c, c + 1)) {
1011 | c += 2;
1012 | }
1013 | else {
1014 | month = anniversary_values[0].substring(c, c + 2);
1015 | c += 3;
1016 | }
1017 | if ("-" == anniversary_values[0].substring(c, c + 1)) {
1018 | c += 2;
1019 | }
1020 | else {
1021 | day = anniversary_values[0].substring(c, c + 2);
1022 | c += 3;
1023 | }
1024 | //
1025 | if (month && day) {
1026 | if (month) {
1027 | serverContact.events[i].date.month = month;
1028 | }
1029 | if (day) {
1030 | serverContact.events[i].date.day = day;
1031 | }
1032 | if (year) {
1033 | serverContact.events[i].date.year = year;
1034 | }
1035 | }
1036 | //
1037 | i++;
1038 | }
1039 | }
1040 | // Set the notes.
1041 | let note_entry = localContact._card.vCardProperties.getFirstEntry("note");
1042 | if (note_entry) {
1043 | serverContact.biographies = [];
1044 | //
1045 | serverContact.biographies[0] = {};
1046 | //
1047 | let note_values = [ note_entry.value ]; // note_entry.value: string
1048 | //
1049 | if (note_values[0]) {
1050 | serverContact.biographies[0].value = note_values[0];
1051 | }
1052 | }
1053 | // Set the organizational properties.
1054 | let title_entry = localContact._card.vCardProperties.getFirstEntry("title");
1055 | let org_entry = localContact._card.vCardProperties.getFirstEntry("org");
1056 | if (title_entry || org_entry) {
1057 | serverContact.organizations = [];
1058 | //
1059 | serverContact.organizations[0] = {};
1060 | //
1061 | if (title_entry) {
1062 | let title_values = [ title_entry.value ]; // title_entry.value: string
1063 | //
1064 | if (title_values[0]) {
1065 | serverContact.organizations[0].title = title_values[0];
1066 | }
1067 | }
1068 | if (org_entry) {
1069 | let org_values = (Array.isArray(org_entry.value) ? org_entry.value : [ org_entry.value ]); // org_entry.value: string or array
1070 | //
1071 | if (org_values[0]) {
1072 | serverContact.organizations[0].name = org_values[0];
1073 | }
1074 | if (org_values[1]) {
1075 | serverContact.organizations[0].department = org_values[1];
1076 | }
1077 | }
1078 | }
1079 | // Set the custom properties.
1080 | let x_custom1_entry = localContact._card.vCardProperties.getFirstEntry("x-custom1");
1081 | let x_custom2_entry = localContact._card.vCardProperties.getFirstEntry("x-custom2");
1082 | let x_custom3_entry = localContact._card.vCardProperties.getFirstEntry("x-custom3");
1083 | let x_custom4_entry = localContact._card.vCardProperties.getFirstEntry("x-custom4");
1084 | if (x_custom1_entry || x_custom2_entry || x_custom3_entry || x_custom4_entry) {
1085 | serverContact.userDefined = [];
1086 | let i = 0;
1087 | //
1088 | if (x_custom1_entry) {
1089 | serverContact.userDefined[i] = {};
1090 | //
1091 | let x_custom1_values = [ x_custom1_entry.value ]; // x_custom1_entry.value: string
1092 | //
1093 | if (x_custom1_values[0]) {
1094 | serverContact.userDefined[i].key = "-";
1095 | serverContact.userDefined[i].value = x_custom1_values[0];
1096 | //
1097 | i++;
1098 | }
1099 | }
1100 | if (x_custom2_entry) {
1101 | serverContact.userDefined[i] = {};
1102 | //
1103 | let x_custom2_values = [ x_custom2_entry.value ]; // x_custom2_entry.value: string
1104 | //
1105 | if (x_custom2_values[0]) {
1106 | serverContact.userDefined[i].key = "-";
1107 | serverContact.userDefined[i].value = x_custom2_values[0];
1108 | //
1109 | i++;
1110 | }
1111 | }
1112 | if (x_custom3_entry) {
1113 | serverContact.userDefined[i] = {};
1114 | //
1115 | let x_custom3_values = [ x_custom3_entry.value ]; // x_custom3_entry.value: string
1116 | //
1117 | if (x_custom3_values[0]) {
1118 | serverContact.userDefined[i].key = "-";
1119 | serverContact.userDefined[i].value = x_custom3_values[0];
1120 | //
1121 | i++;
1122 | }
1123 | }
1124 | if (x_custom4_entry) {
1125 | serverContact.userDefined[i] = {};
1126 | //
1127 | let x_custom4_values = [ x_custom4_entry.value ]; // x_custom4_entry.value: string
1128 | //
1129 | if (x_custom4_values[0]) {
1130 | serverContact.userDefined[i].key = "-";
1131 | serverContact.userDefined[i].value = x_custom4_values[0];
1132 | //
1133 | i++;
1134 | }
1135 | }
1136 | }
1137 | //
1138 | return serverContact;
1139 | }
1140 |
1141 | /* Contact groups. */
1142 |
1143 | static async synchronizeContactGroups(peopleAPI, targetAddressBook, targetAddressBookItemMap, addedLocalItemIds, modifiedLocalItemIds, deletedLocalItemIds, readOnlyMode) {
1144 | if (null == peopleAPI) {
1145 | throw new IllegalArgumentError("Invalid 'peopleAPI': null.");
1146 | }
1147 | if (null == targetAddressBook) {
1148 | throw new IllegalArgumentError("Invalid 'targetAddressBook': null.");
1149 | }
1150 | if (null == targetAddressBookItemMap) {
1151 | throw new IllegalArgumentError("Invalid 'targetAddressBookItemMap': null.");
1152 | }
1153 | if (null == addedLocalItemIds) {
1154 | throw new IllegalArgumentError("Invalid 'addedLocalItemIds': null.");
1155 | }
1156 | if (null == modifiedLocalItemIds) {
1157 | throw new IllegalArgumentError("Invalid 'modifiedLocalItemIds': null.");
1158 | }
1159 | if (null == deletedLocalItemIds) {
1160 | throw new IllegalArgumentError("Invalid 'deletedLocalItemIds': null.");
1161 | }
1162 | if (null == readOnlyMode) {
1163 | throw new IllegalArgumentError("Invalid 'readOnlyMode': null.");
1164 | }
1165 | // Retrieve all server contact groups.
1166 | let serverContactGroups = await peopleAPI.getContactGroups();
1167 | // Cycle on the server contact groups.
1168 | logger.log0("AddressBookSynchronizer.synchronizeContactGroups(): Cycling on the server contact groups.");
1169 | let includeSystemContactGroups = peopleAPI.getIncludeSystemContactGroups();
1170 | logger.log1("PeopleAPI.getContactGroups(): includeSystemContactGroups = " + includeSystemContactGroups);
1171 | for (let serverContactGroup of serverContactGroups) {
1172 | // Get the resource name (in the form 'contactGroups/contactGroupId') and the name.
1173 | let resourceName = serverContactGroup.resourceName;
1174 | let name = serverContactGroup.name;
1175 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ")");
1176 | // Determine if the server contact group is a system one and if it should be discarded.
1177 | if (("SYSTEM_CONTACT_GROUP" === serverContactGroup.groupType) && (!includeSystemContactGroups)) {
1178 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") is a system contact group and has therefore been ignored.");
1179 | continue;
1180 | }
1181 | // Try to match the server contact group locally.
1182 | let localContactGroup = targetAddressBookItemMap.get(resourceName);
1183 | // If such a local contact group is currently unavailable...
1184 | if (undefined === localContactGroup) {
1185 | // ...and if it was previously deleted locally...
1186 | if (deletedLocalItemIds.includes(resourceName)) {
1187 | // Check we are not in read-only mode, then...
1188 | if (!readOnlyMode) {
1189 | // Delete the server contact group remotely.
1190 | await peopleAPI.deleteContactGroup(resourceName);
1191 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") has been deleted remotely.");
1192 | }
1193 | // Remove the resource name from the local change log (deleted items).
1194 | await targetAddressBook.removeItemFromChangeLog(resourceName);
1195 | }
1196 | // ...and if it wasn't previously deleted locally...
1197 | else {
1198 | // Create a new local contact group.
1199 | localContactGroup = targetAddressBook.createNewList();
1200 | // Import the server contact group information into the local contact group.
1201 | localContactGroup.setProperty("X-GOOGLE-RESOURCENAME", resourceName);
1202 | localContactGroup.setProperty("X-GOOGLE-ETAG", serverContactGroup.etag);
1203 | localContactGroup = AddressBookSynchronizer.fillLocalContactGroupWithServerContactGroupInformation(localContactGroup, serverContactGroup);
1204 | // Add the local contact group locally, and keep the target address book item map up-to-date.
1205 | await targetAddressBook.addItem(localContactGroup, true);
1206 | targetAddressBookItemMap.set(resourceName, localContactGroup);
1207 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") has been added locally.");
1208 | // Remove the resource name from the local change log (added items).
1209 | // (This should be logically useless, but sometimes the change log is filled with some of the contact groups added above.)
1210 | await targetAddressBook.removeItemFromChangeLog(resourceName);
1211 | }
1212 | }
1213 | // If such a local contact group is currently available...
1214 | else {
1215 | // ...and if the server one is more recent, or if we are in read-only mode...
1216 | if ((localContactGroup.getProperty("X-GOOGLE-ETAG") !== serverContactGroup.etag) || (readOnlyMode)) {
1217 | // Import the server contact group information into the local contact group.
1218 | localContactGroup.setProperty("X-GOOGLE-ETAG", serverContactGroup.etag);
1219 | localContactGroup = AddressBookSynchronizer.fillLocalContactGroupWithServerContactGroupInformation(localContactGroup, serverContactGroup);
1220 | // Update the local contact group locally, and keep the target address book item map up-to-date.
1221 | await targetAddressBook.modifyItem(localContactGroup, true);
1222 | targetAddressBookItemMap.set(resourceName, localContactGroup);
1223 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") has been updated locally.");
1224 | // Remove the resource name from the local change log (modified items).
1225 | await targetAddressBook.removeItemFromChangeLog(resourceName);
1226 | }
1227 | }
1228 | }
1229 | // Prepare the variables for the cycles.
1230 | let addedLocalItemResourceNames = new Set();
1231 | // Cycle on the locally added contact groups.
1232 | logger.log0("AddressBookSynchronizer.synchronizeContactGroups(): Cycling on the locally added contact groups.");
1233 | for (let localContactGroupId of addedLocalItemIds) {
1234 | // Retrieve the local contact group, and make sure such an item is actually valid and a real contact group.
1235 | let localContactGroup = targetAddressBookItemMap.get(localContactGroupId);
1236 | if (undefined === localContactGroup) {
1237 | continue;
1238 | }
1239 | if (!localContactGroup.isMailList) {
1240 | continue;
1241 | }
1242 | // Check we are not in read-only mode, then...
1243 | if (!readOnlyMode) {
1244 | // Create a new server contact group.
1245 | let serverContactGroup = {};
1246 | // Import the local contact group information into the server contact group.
1247 | serverContactGroup = AddressBookSynchronizer.fillServerContactGroupWithLocalContactGroupInformation(localContactGroup, serverContactGroup);
1248 | // Add the server contact group remotely and get the resource name (in the form 'contactGroups/contactGroupId') and the name.
1249 | serverContactGroup = await peopleAPI.createContactGroup(serverContactGroup);
1250 | let resourceName = serverContactGroup.resourceName;
1251 | let name = serverContactGroup.name;
1252 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") has been added remotely.");
1253 | // Import the server contact group information into the local contact group.
1254 | localContactGroup.setProperty("X-GOOGLE-RESOURCENAME", resourceName);
1255 | localContactGroup.setProperty("X-GOOGLE-ETAG", serverContactGroup.etag);
1256 | localContactGroup = AddressBookSynchronizer.fillLocalContactGroupWithServerContactGroupInformation(localContactGroup, serverContactGroup);
1257 | // Update the local contact group locally, and keep the target address book item map up-to-date.
1258 | await targetAddressBook.modifyItem(localContactGroup, true);
1259 | targetAddressBookItemMap.set(resourceName, localContactGroup);
1260 | // Add the resource name to the proper set.
1261 | addedLocalItemResourceNames.add(resourceName);
1262 | }
1263 | // Remove the local contact group id from the local change log (added items).
1264 | await targetAddressBook.removeItemFromChangeLog(localContactGroupId);
1265 | }
1266 | // Cycle on the locally modified contact groups.
1267 | logger.log0("AddressBookSynchronizer.synchronizeContactGroups(): Cycling on the locally modified contact groups.");
1268 | for (let localContactGroupId of modifiedLocalItemIds) {
1269 | // Retrieve the local contact group, and make sure such an item is actually valid and a real contact group.
1270 | let localContactGroup = targetAddressBookItemMap.get(localContactGroupId);
1271 | if (undefined === localContactGroup) {
1272 | continue;
1273 | }
1274 | if (!localContactGroup.isMailList) {
1275 | continue;
1276 | }
1277 | // Check we are not in read-only mode, then...
1278 | if (!readOnlyMode) {
1279 | // Make sure the local contact group has a valid X-GOOGLE-ETAG property (if not, it is probably a system contact group, which cannot be updated).
1280 | if (!localContactGroup.getProperty("X-GOOGLE-ETAG")) {
1281 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + localContactGroupId + " has no X-GOOGLE-ETAG property and has therefore been ignored.");
1282 | continue;
1283 | }
1284 | // Create a new server contact group.
1285 | let serverContactGroup = {};
1286 | serverContactGroup.resourceName = localContactGroup.getProperty("X-GOOGLE-RESOURCENAME");
1287 | serverContactGroup.etag = localContactGroup.getProperty("X-GOOGLE-ETAG");
1288 | // Import the local contact group information into the server contact group.
1289 | serverContactGroup = AddressBookSynchronizer.fillServerContactGroupWithLocalContactGroupInformation(localContactGroup, serverContactGroup);
1290 | // Update the server contact group remotely or delete the local contact group locally.
1291 | try {
1292 | // Update the server contact group remotely and get the resource name (in the form 'contactGroups/contactGroupId') and the name.
1293 | serverContactGroup = await peopleAPI.updateContactGroup(serverContactGroup);
1294 | let resourceName = serverContactGroup.resourceName;
1295 | let name = serverContactGroup.name;
1296 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") has been updated remotely.");
1297 | // Import the server contact group information into the local contact group.
1298 | localContactGroup.setProperty("X-GOOGLE-RESOURCENAME", resourceName);
1299 | localContactGroup.setProperty("X-GOOGLE-ETAG", serverContactGroup.etag);
1300 | localContactGroup = AddressBookSynchronizer.fillLocalContactGroupWithServerContactGroupInformation(localContactGroup, serverContactGroup);
1301 | // Update the local contact group locally, and keep the target address book item map up-to-date.
1302 | await targetAddressBook.modifyItem(localContactGroup, true);
1303 | targetAddressBookItemMap.set(resourceName, localContactGroup);
1304 | }
1305 | catch (error) {
1306 | // If the server contact group is no longer available (i.e.: it was deleted)...
1307 | if ((error instanceof ResponseError) && (error.message.includes(": 404:"))) {
1308 | // Get the resource name (in the form 'contactGroups/contactGroupId').
1309 | let resourceName = localContactGroup.getProperty("X-GOOGLE-RESOURCENAME");
1310 | let name = localContactGroup.getProperty("ListName");
1311 | // Delete the local contact group locally, and keep the target address book item map up-to-date.
1312 | /* FIXME: temporary: .deleteItem() does not actually delete a contact group.
1313 | targetAddressBook.deleteItem(localContactGroup, true);
1314 | */
1315 | let abManager = Components.classes["@mozilla.org/abmanager;1"].createInstance(Components.interfaces.nsIAbManager);
1316 | abManager.deleteAddressBook(localContactGroup._card.mailListURI);
1317 | targetAddressBookItemMap.delete(resourceName);
1318 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + resourceName + " (" + name + ") has been deleted locally.");
1319 | }
1320 | // If the root reason is different...
1321 | else {
1322 | // Propagate the error.
1323 | throw error;
1324 | }
1325 | }
1326 | }
1327 | // Remove the local contact group id from the local change log (modified items).
1328 | await targetAddressBook.removeItemFromChangeLog(localContactGroupId);
1329 | }
1330 | // Determine all the contact groups which were previously deleted remotely and delete them locally.
1331 | logger.log0("AddressBookSynchronizer.synchronizeContactGroups(): Determining all the remotely deleted contact groups.");
1332 | for (let localContactGroup of targetAddressBookItemMap.values()) {
1333 | // Make sure the item is actually valid and a real contact group.
1334 | if (undefined === targetAddressBookItemMap.get(localContactGroup.getProperty("X-GOOGLE-RESOURCENAME"))) {
1335 | continue;
1336 | }
1337 | if (!localContactGroup.isMailList) {
1338 | continue;
1339 | }
1340 | // Get the local contact group id and the name.
1341 | let localContactGroupId = localContactGroup.getProperty("X-GOOGLE-RESOURCENAME");
1342 | let name = localContactGroup.getProperty("ListName");
1343 | // Check if the local contact group id matches any of the locally added contact groups.
1344 | if (addedLocalItemResourceNames.has(localContactGroupId)) {
1345 | continue;
1346 | }
1347 | // Check if the local contact group id matches any of the resource names downloaded.
1348 | let localContactGroupFoundAmongServerContactGroups = false;
1349 | for (let serverContactGroup of serverContactGroups) {
1350 | if (localContactGroupId === serverContactGroup.resourceName) {
1351 | localContactGroupFoundAmongServerContactGroups = true;
1352 | break;
1353 | }
1354 | }
1355 | if (localContactGroupFoundAmongServerContactGroups) {
1356 | continue;
1357 | }
1358 | // Delete the local contact group locally, and keep the target address book item map up-to-date.
1359 | /* FIXME: temporary: .deleteItem() does not actually delete a contact group.
1360 | targetAddressBook.deleteItem(localContactGroup, true);
1361 | */
1362 | let abManager = Components.classes["@mozilla.org/abmanager;1"].createInstance(Components.interfaces.nsIAbManager);
1363 | abManager.deleteAddressBook(localContactGroup._card.mailListURI);
1364 | targetAddressBookItemMap.delete(localContactGroupId);
1365 | logger.log1("AddressBookSynchronizer.synchronizeContactGroups(): " + localContactGroupId + " (" + name + ") has been deleted locally.");
1366 | }
1367 | }
1368 |
1369 | static fillLocalContactGroupWithServerContactGroupInformation(localContactGroup, serverContactGroup) {
1370 | if (null == localContactGroup) {
1371 | throw new IllegalArgumentError("Invalid 'localContactGroup': null.");
1372 | }
1373 | if (null == serverContactGroup) {
1374 | throw new IllegalArgumentError("Invalid 'serverContactGroup': null.");
1375 | }
1376 | // Reset all the properties managed by this method.
1377 | localContactGroup.deleteProperty("ListName");
1378 | // Set the name.
1379 | if (serverContactGroup.name) {
1380 | let name = serverContactGroup.name.replace(/[<>;,"]/g, '_');
1381 | //
1382 | localContactGroup.setProperty("ListName", name);
1383 | }
1384 | //
1385 | return localContactGroup;
1386 | }
1387 |
1388 | static fillServerContactGroupWithLocalContactGroupInformation(localContactGroup, serverContactGroup) {
1389 | if (null == localContactGroup) {
1390 | throw new IllegalArgumentError("Invalid 'localContactGroup': null.");
1391 | }
1392 | if (null == serverContactGroup) {
1393 | throw new IllegalArgumentError("Invalid 'serverContactGroup': null.");
1394 | }
1395 | // Reset all the properties managed by this method.
1396 | delete serverContactGroup.name;
1397 | // Set the name.
1398 | if (localContactGroup.getProperty("ListName")) {
1399 | serverContactGroup.name = localContactGroup.getProperty("ListName");
1400 | }
1401 | //
1402 | return serverContactGroup;
1403 | }
1404 |
1405 | /* Contact group members. */
1406 |
1407 | static async synchronizeContactGroupMembers(targetAddressBook, targetAddressBookItemMap, contactGroupMemberMap, readOnlyMode) { // https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Address_Book_Examples
1408 | if (null == targetAddressBook) {
1409 | throw new IllegalArgumentError("Invalid 'targetAddressBook': null.");
1410 | }
1411 | if (null == targetAddressBookItemMap) {
1412 | throw new IllegalArgumentError("Invalid 'targetAddressBookItemMap': null.");
1413 | }
1414 | if (null == contactGroupMemberMap) {
1415 | throw new IllegalArgumentError("Invalid 'contactGroupMemberMap': null.");
1416 | }
1417 | if (null == readOnlyMode) {
1418 | throw new IllegalArgumentError("Invalid 'readOnlyMode': null.");
1419 | }
1420 | // FIXME: temporary (Google-to-Thunderbird only synchronization).
1421 | // Cycle on the local contact groups.
1422 | logger.log0("AddressBookSynchronizer.synchronizeContactGroupMembers(): Determining all the members for each contact group.");
1423 | for (let localContactGroup of targetAddressBookItemMap.values()) {
1424 | // Make sure the item is actually valid and a real contact group.
1425 | if (undefined === targetAddressBookItemMap.get(localContactGroup.getProperty("X-GOOGLE-RESOURCENAME"))) {
1426 | continue;
1427 | }
1428 | if (!localContactGroup.isMailList) {
1429 | continue;
1430 | }
1431 | // Retrieve the local contact group directory.
1432 | let abManager = Components.classes["@mozilla.org/abmanager;1"].getService(Components.interfaces.nsIAbManager);
1433 | let localContactGroupDirectory = abManager.getDirectory(localContactGroup._card.mailListURI);
1434 | // Retrieve the server contact group members.
1435 | let serverContactGroupMembers = contactGroupMemberMap.get(localContactGroup.getProperty("X-GOOGLE-RESOURCENAME"));
1436 | // Synchronize the local contact group members with the (old) server contact group members.
1437 | for (let localContactGroupDirectoryCard of localContactGroupDirectory.childCards) {
1438 | let localContactGroupDirectoryCardResourceName = localContactGroupDirectoryCard.getProperty("X-GOOGLE-RESOURCENAME", null);
1439 | if ((undefined !== serverContactGroupMembers) && (serverContactGroupMembers.has(localContactGroupDirectoryCardResourceName))) {
1440 | serverContactGroupMembers.delete(localContactGroupDirectoryCardResourceName);
1441 | }
1442 | else {
1443 | localContactGroupDirectory.deleteCards([ localContactGroupDirectoryCard ]);
1444 | }
1445 | }
1446 | // Fill the local contact group with the remaining (new) server contact group members.
1447 | if (undefined !== serverContactGroupMembers) {
1448 | for (let serverContactGroupMember of serverContactGroupMembers) {
1449 | let localContact = targetAddressBookItemMap.get(serverContactGroupMember);
1450 | localContactGroupDirectory.addCard(localContact._card);
1451 | }
1452 | }
1453 | // Finalize the changes.
1454 | localContactGroupDirectory.editMailListToDatabase(localContactGroup);
1455 | }
1456 | }
1457 |
1458 | static updateContactGroupMemberMap(contactGroupMemberMap, contactResourceName, contactMemberships) {
1459 | if (null == contactGroupMemberMap) {
1460 | throw new IllegalArgumentError("Invalid 'contactGroupMemberMap': null.");
1461 | }
1462 | if (null == contactResourceName) {
1463 | throw new IllegalArgumentError("Invalid 'contactResourceName': null.");
1464 | }
1465 | if (null == contactMemberships) {
1466 | throw new IllegalArgumentError("Invalid 'contactMemberships': null.");
1467 | }
1468 | // Cycle on all contact memberships.
1469 | for (let contactMembership of contactMemberships) {
1470 | // Discard useless items.
1471 | if (undefined == contactMembership.contactGroupMembership) {
1472 | continue;
1473 | }
1474 | // Retrieve the contact group resource name.
1475 | let contactGroupResourceName = contactMembership.contactGroupMembership.contactGroupResourceName;
1476 | // If such a contact group is not already in the map, add it and its set.
1477 | if (undefined === contactGroupMemberMap.get(contactGroupResourceName)) {
1478 | contactGroupMemberMap.set(contactGroupResourceName, new Set());
1479 | }
1480 | // Add the contact to the map.
1481 | contactGroupMemberMap.get(contactGroupResourceName).add(contactResourceName);
1482 | }
1483 | }
1484 |
1485 | /* Change log. */
1486 |
1487 | static async fixChangeLog(targetAddressBook, targetAddressBookItemMap) {
1488 | if (null == targetAddressBook) {
1489 | throw new IllegalArgumentError("Invalid 'targetAddressBook': null.");
1490 | }
1491 | if (null == targetAddressBookItemMap) {
1492 | throw new IllegalArgumentError("Invalid 'targetAddressBookItemMap': null.");
1493 | }
1494 | // Cycle on all the items in the change log.
1495 | logger.log0("AddressBookSynchronizer.synchronize(): Fixing the change log.");
1496 | for (let item of targetAddressBook.getItemsFromChangeLog()) {
1497 | // Retrieve the item id.
1498 | let itemId = item.itemId;
1499 | // Try to match the item locally.
1500 | /* FIXME: this query must be done on the current target address book.
1501 | let localItem = targetAddressBookItemMap.get(itemId);
1502 | */
1503 | let localItem = await targetAddressBook.getItemFromProperty("X-GOOGLE-RESOURCENAME", itemId);
1504 | // If it is not a valid local item...
1505 | /* FIXME.
1506 | if (undefined === localItem) {
1507 | */
1508 | if (null === localItem) {
1509 | // Remove the item id from the change log.
1510 | await targetAddressBook.removeItemFromChangeLog(itemId);
1511 | logger.log1("AddressBookSynchronizer.fixChangeLog(): " + itemId + " has been removed from the change log.");
1512 | }
1513 | }
1514 | }
1515 |
1516 | }
1517 |
--------------------------------------------------------------------------------