summaryrefslogtreecommitdiffstats
path: root/comm/mail/services/sync
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/services/sync')
-rw-r--r--comm/mail/services/sync/modules/engines/accounts.sys.mjs392
-rw-r--r--comm/mail/services/sync/modules/engines/addressBooks.sys.mjs380
-rw-r--r--comm/mail/services/sync/modules/engines/calendars.sys.mjs349
-rw-r--r--comm/mail/services/sync/modules/engines/identities.sys.mjs394
-rw-r--r--comm/mail/services/sync/moz.build14
-rw-r--r--comm/mail/services/sync/test/unit/head.js7
-rw-r--r--comm/mail/services/sync/test/unit/test_account_store.js361
-rw-r--r--comm/mail/services/sync/test/unit/test_account_tracker.js155
-rw-r--r--comm/mail/services/sync/test/unit/test_addressBook_store.js130
-rw-r--r--comm/mail/services/sync/test/unit/test_addressBook_tracker.js167
-rw-r--r--comm/mail/services/sync/test/unit/test_calendar_store.js180
-rw-r--r--comm/mail/services/sync/test/unit/test_calendar_tracker.js154
-rw-r--r--comm/mail/services/sync/test/unit/test_identity_store.js222
-rw-r--r--comm/mail/services/sync/test/unit/test_identity_tracker.js238
-rw-r--r--comm/mail/services/sync/test/unit/xpcshell.ini11
15 files changed, 3154 insertions, 0 deletions
diff --git a/comm/mail/services/sync/modules/engines/accounts.sys.mjs b/comm/mail/services/sync/modules/engines/accounts.sys.mjs
new file mode 100644
index 0000000000..d504da0fee
--- /dev/null
+++ b/comm/mail/services/sync/modules/engines/accounts.sys.mjs
@@ -0,0 +1,392 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
+import {
+ Store,
+ SyncEngine,
+ Tracker,
+} from "resource://services-sync/engines.sys.mjs";
+import { Utils } from "resource://services-sync/util.sys.mjs";
+
+const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
+ "resource://services-sync/constants.js"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const SYNCED_SMTP_PROPERTIES = {
+ authMethod: "authMethod",
+ port: "port",
+ description: "name",
+ socketType: "socketType",
+};
+
+const SYNCED_SERVER_PROPERTIES = {
+ authMethod: "authMethod",
+ biffMinutes: "check_time",
+ doBiff: "check_new_mail",
+ downloadOnBiff: "download_on_biff",
+ emptyTrashOnExit: "empty_trash_on_exit",
+ incomingDuplicateAction: "dup_action",
+ limitOfflineMessageSize: "limit_offline_message_size",
+ loginAtStartUp: "login_at_startup",
+ maxMessageSize: "max_size",
+ port: "port",
+ prettyName: "name",
+ socketType: "socketType",
+};
+
+/**
+ * AccountRecord represents the state of an add-on in an application.
+ *
+ * Each add-on has its own record for each application ID it is installed
+ * on.
+ *
+ * The ID of add-on records is a randomly-generated GUID. It is random instead
+ * of deterministic so the URIs of the records cannot be guessed and so
+ * compromised server credentials won't result in disclosure of the specific
+ * add-ons present in a Sync account.
+ *
+ * The record contains the following fields:
+ *
+ */
+export function AccountRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+
+AccountRecord.prototype = {
+ __proto__: CryptoWrapper.prototype,
+ _logName: "Record.Account",
+};
+Utils.deferGetSet(AccountRecord, "cleartext", [
+ "username",
+ "hostname",
+ "type",
+ "prefs",
+ "isDefault",
+]);
+
+export function AccountsEngine(service) {
+ SyncEngine.call(this, "Accounts", service);
+}
+
+AccountsEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: AccountStore,
+ _trackerObj: AccountTracker,
+ _recordObj: AccountRecord,
+ version: 1,
+ syncPriority: 3,
+
+ /*
+ * Returns a changeset for this sync. Engine implementations can override this
+ * method to bypass the tracker for certain or all changed items.
+ */
+ async getChangedIDs() {
+ return this._tracker.getChangedIDs();
+ },
+};
+
+function AccountStore(name, engine) {
+ Store.call(this, name, engine);
+}
+AccountStore.prototype = {
+ __proto__: Store.prototype,
+
+ /**
+ * Create an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to create an item from
+ */
+ async create(record) {
+ if (record.type == "smtp") {
+ let smtpServer = MailServices.smtp.createServer();
+ smtpServer.UID = record.id;
+ smtpServer.username = record.username;
+ smtpServer.hostname = record.hostname;
+ for (let key of Object.keys(SYNCED_SMTP_PROPERTIES)) {
+ if (key in record.prefs) {
+ smtpServer[key] = record.prefs[key];
+ }
+ }
+ if (record.isDefault) {
+ MailServices.smtp.defaultServer = smtpServer;
+ }
+ return;
+ }
+
+ try {
+ // Ensure there is a local mail account...
+ MailServices.accounts.localFoldersServer;
+ } catch {
+ // ... if not, make one.
+ MailServices.accounts.createLocalMailAccount();
+ }
+
+ let server = MailServices.accounts.createIncomingServer(
+ record.username,
+ record.hostname,
+ record.type
+ );
+ server.UID = record.id;
+
+ for (let key of Object.keys(SYNCED_SERVER_PROPERTIES)) {
+ if (key in record.prefs) {
+ server[key] = record.prefs[key];
+ }
+ }
+
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+
+ if (server.loginAtStartUp) {
+ Services.wm
+ .getMostRecentWindow("mail:3pane")
+ ?.GetNewMsgs(server, server.rootFolder);
+ }
+ },
+
+ /**
+ * Remove an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to delete an item from
+ */
+ async remove(record) {
+ let smtpServer = MailServices.smtp.servers.find(s => s.UID == record.id);
+ if (smtpServer) {
+ MailServices.smtp.deleteServer(smtpServer);
+ return;
+ }
+
+ let server = MailServices.accounts.allServers.find(s => s.UID == record.id);
+ if (!server) {
+ this._log.trace("Asked to remove record that doesn't exist, ignoring");
+ return;
+ }
+
+ let account = MailServices.accounts.FindAccountForServer(server);
+ if (account) {
+ MailServices.accounts.removeAccount(account, true);
+ } else {
+ // Is this even possible?
+ MailServices.accounts.removeIncomingServer(account, true);
+ }
+ },
+
+ /**
+ * Update an item from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The record to use to update an item from
+ */
+ async update(record) {
+ if (record.type == "smtp") {
+ await this._updateSMTP(record);
+ return;
+ }
+
+ await this._updateIncoming(record);
+ },
+
+ async _updateSMTP(record) {
+ let smtpServer = MailServices.smtp.servers.find(s => s.UID == record.id);
+ if (!smtpServer) {
+ this._log.trace("Skipping update for unknown item: " + record.id);
+ return;
+ }
+ smtpServer.username = record.username;
+ smtpServer.hostname = record.hostname;
+ for (let key of Object.keys(SYNCED_SMTP_PROPERTIES)) {
+ if (key in record.prefs) {
+ smtpServer[key] = record.prefs[key];
+ }
+ }
+ if (record.isDefault) {
+ MailServices.smtp.defaultServer = smtpServer;
+ }
+ },
+
+ async _updateIncoming(record) {
+ let server = MailServices.accounts.allServers.find(s => s.UID == record.id);
+ if (!server) {
+ this._log.trace("Skipping update for unknown item: " + record.id);
+ return;
+ }
+ if (server.type != record.type) {
+ throw new Components.Exception(
+ `Refusing to change server type from ${server.type} to ${record.type}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ for (let key of Object.keys(SYNCED_SERVER_PROPERTIES)) {
+ if (key in record.prefs) {
+ server[key] = record.prefs[key];
+ }
+ }
+ },
+
+ /**
+ * Determine whether a record with the specified ID exists.
+ *
+ * Takes a string record ID and returns a booleans saying whether the record
+ * exists.
+ *
+ * @param id
+ * string record ID
+ * @return boolean indicating whether record exists locally
+ */
+ async itemExists(id) {
+ return id in (await this.getAllIDs());
+ },
+
+ /**
+ * Obtain the set of all known record IDs.
+ *
+ * @return Object with ID strings as keys and values of true. The values
+ * are ignored.
+ */
+ async getAllIDs() {
+ let ids = {};
+ for (let s of MailServices.smtp.servers) {
+ ids[s.UID] = true;
+ }
+ for (let s of MailServices.accounts.allServers) {
+ if (["imap", "pop3"].includes(s.type)) {
+ ids[s.UID] = true;
+ }
+ }
+ return ids;
+ },
+
+ /**
+ * Create a record from the specified ID.
+ *
+ * If the ID is known, the record should be populated with metadata from
+ * the store. If the ID is not known, the record should be created with the
+ * delete field set to true.
+ *
+ * @param id
+ * string record ID
+ * @param collection
+ * Collection to add record to. This is typically passed into the
+ * constructor for the newly-created record.
+ * @return record type for this engine
+ */
+ async createRecord(id, collection) {
+ let record = new AccountRecord(collection, id);
+
+ let server = MailServices.smtp.servers.find(s => s.UID == id);
+ if (server) {
+ record.type = "smtp";
+ record.username = server.username;
+ record.hostname = server.hostname;
+ record.prefs = {};
+ for (let key of Object.keys(SYNCED_SMTP_PROPERTIES)) {
+ record.prefs[key] = server[key];
+ }
+ record.isDefault = MailServices.smtp.defaultServer == server;
+ return record;
+ }
+
+ server = MailServices.accounts.allServers.find(s => s.UID == id);
+ // If we don't know about this ID, mark the record as deleted.
+ if (!server) {
+ record.deleted = true;
+ return record;
+ }
+
+ record.type = server.type;
+ record.username = server.username;
+ record.hostname = server.hostName;
+ record.prefs = {};
+ for (let key of Object.keys(SYNCED_SERVER_PROPERTIES)) {
+ record.prefs[key] = server[key];
+ }
+
+ return record;
+ },
+};
+
+function AccountTracker(name, engine) {
+ Tracker.call(this, name, engine);
+}
+AccountTracker.prototype = {
+ __proto__: Tracker.prototype,
+
+ _changedIDs: new Set(),
+ _ignoreAll: false,
+
+ async getChangedIDs() {
+ let changes = {};
+ for (let id of this._changedIDs) {
+ changes[id] = 0;
+ }
+ return changes;
+ },
+
+ clearChangedIDs() {
+ this._changedIDs.clear();
+ },
+
+ get ignoreAll() {
+ return this._ignoreAll;
+ },
+
+ set ignoreAll(value) {
+ this._ignoreAll = value;
+ },
+
+ onStart() {
+ Services.prefs.addObserver("mail.server.", this);
+ Services.obs.addObserver(this, "message-server-removed");
+ },
+
+ onStop() {
+ Services.prefs.removeObserver("mail.server.", this);
+ Services.obs.removeObserver(this, "message-server-removed");
+ },
+
+ observe(subject, topic, data) {
+ if (this._ignoreAll) {
+ return;
+ }
+
+ let server;
+ if (topic == "message-server-removed") {
+ server = subject.QueryInterface(Ci.nsIMsgIncomingServer);
+ } else {
+ let serverKey = data.split(".")[2];
+ let prefName = data.substring(serverKey.length + 13);
+ if (!Object.values(SYNCED_SERVER_PROPERTIES).includes(prefName)) {
+ return;
+ }
+
+ // Don't use getIncomingServer or it'll throw if the server doesn't exist.
+ server = MailServices.accounts.allServers.find(s => s.key == serverKey);
+ }
+
+ if (
+ server &&
+ ["imap", "pop3"].includes(server.type) &&
+ !this._changedIDs.has(server.UID)
+ ) {
+ this._changedIDs.add(server.UID);
+ this.score += SCORE_INCREMENT_XLARGE;
+ }
+ },
+};
diff --git a/comm/mail/services/sync/modules/engines/addressBooks.sys.mjs b/comm/mail/services/sync/modules/engines/addressBooks.sys.mjs
new file mode 100644
index 0000000000..9df9f678df
--- /dev/null
+++ b/comm/mail/services/sync/modules/engines/addressBooks.sys.mjs
@@ -0,0 +1,380 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
+import {
+ Store,
+ SyncEngine,
+ Tracker,
+} from "resource://services-sync/engines.sys.mjs";
+import { Utils } from "resource://services-sync/util.sys.mjs";
+
+const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
+ "resource://services-sync/constants.js"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const SYNCED_COMMON_PROPERTIES = {
+ autocomplete: "enable_autocomplete",
+ readOnly: "readOnly",
+};
+
+const SYNCED_CARDDAV_PROPERTIES = {
+ syncInterval: "carddav.syncinterval",
+ url: "carddav.url",
+ username: "carddav.username",
+};
+
+const SYNCED_LDAP_PROPERTIES = {
+ protocolVersion: "protocolVersion",
+ authSASLMechanism: "auth.saslmech",
+ authDN: "auth.dn",
+ uri: "uri",
+ maxHits: "maxHits",
+};
+
+/**
+ * AddressBookRecord represents the state of an add-on in an application.
+ *
+ * Each add-on has its own record for each application ID it is installed
+ * on.
+ *
+ * The ID of add-on records is a randomly-generated GUID. It is random instead
+ * of deterministic so the URIs of the records cannot be guessed and so
+ * compromised server credentials won't result in disclosure of the specific
+ * add-ons present in a Sync account.
+ *
+ * The record contains the following fields:
+ *
+ */
+export function AddressBookRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+
+AddressBookRecord.prototype = {
+ __proto__: CryptoWrapper.prototype,
+ _logName: "Record.AddressBook",
+};
+Utils.deferGetSet(AddressBookRecord, "cleartext", ["name", "type", "prefs"]);
+
+export function AddressBooksEngine(service) {
+ SyncEngine.call(this, "AddressBooks", service);
+}
+
+AddressBooksEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: AddressBookStore,
+ _trackerObj: AddressBookTracker,
+ _recordObj: AddressBookRecord,
+ version: 1,
+ syncPriority: 6,
+
+ /*
+ * Returns a changeset for this sync. Engine implementations can override this
+ * method to bypass the tracker for certain or all changed items.
+ */
+ async getChangedIDs() {
+ return this._tracker.getChangedIDs();
+ },
+};
+
+function AddressBookStore(name, engine) {
+ Store.call(this, name, engine);
+}
+AddressBookStore.prototype = {
+ __proto__: Store.prototype,
+
+ _addPrefsToBook(book, record, whichPrefs) {
+ for (let [key, realKey] of Object.entries(whichPrefs)) {
+ let value = record.prefs[key];
+ let type = typeof value;
+ if (type == "string") {
+ book.setStringValue(realKey, value);
+ } else if (type == "number") {
+ book.setIntValue(realKey, value);
+ } else if (type == "boolean") {
+ book.setBoolValue(realKey, value);
+ }
+ }
+ },
+
+ /**
+ * Create an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to create an item from
+ */
+ async create(record) {
+ if (
+ ![
+ MailServices.ab.LDAP_DIRECTORY_TYPE,
+ MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ ].includes(record.type)
+ ) {
+ return;
+ }
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ record.name,
+ null,
+ record.type,
+ record.id
+ );
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+
+ this._addPrefsToBook(book, record, SYNCED_COMMON_PROPERTIES);
+ if (record.type == MailServices.ab.CARDDAV_DIRECTORY_TYPE) {
+ this._addPrefsToBook(book, record, SYNCED_CARDDAV_PROPERTIES);
+ book.wrappedJSObject.fetchAllFromServer();
+ } else if (record.type == MailServices.ab.LDAP_DIRECTORY_TYPE) {
+ this._addPrefsToBook(book, record, SYNCED_LDAP_PROPERTIES);
+ }
+ },
+
+ /**
+ * Remove an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to delete an item from
+ */
+ async remove(record) {
+ let book = MailServices.ab.getDirectoryFromUID(record.id);
+ if (!book) {
+ this._log.trace("Asked to remove record that doesn't exist, ignoring");
+ return;
+ }
+
+ let deletedPromise = new Promise(resolve => {
+ Services.obs.addObserver(
+ {
+ observe() {
+ Services.obs.removeObserver(this, "addrbook-directory-deleted");
+ resolve();
+ },
+ },
+ "addrbook-directory-deleted"
+ );
+ });
+ MailServices.ab.deleteAddressBook(book.URI);
+ await deletedPromise;
+ },
+
+ /**
+ * Update an item from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The record to use to update an item from
+ */
+ async update(record) {
+ let book = MailServices.ab.getDirectoryFromUID(record.id);
+ if (!book) {
+ this._log.trace("Skipping update for unknown item: " + record.id);
+ return;
+ }
+ if (book.dirType != record.type) {
+ throw new Components.Exception(
+ `Refusing to change book type from ${book.dirType} to ${record.type}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ if (book.dirName != record.name) {
+ book.dirName = record.name;
+ }
+ this._addPrefsToBook(book, record, SYNCED_COMMON_PROPERTIES);
+ if (record.type == MailServices.ab.CARDDAV_DIRECTORY_TYPE) {
+ this._addPrefsToBook(book, record, SYNCED_CARDDAV_PROPERTIES);
+ } else if (record.type == MailServices.ab.LDAP_DIRECTORY_TYPE) {
+ this._addPrefsToBook(book, record, SYNCED_LDAP_PROPERTIES);
+ }
+ },
+
+ /**
+ * Determine whether a record with the specified ID exists.
+ *
+ * Takes a string record ID and returns a booleans saying whether the record
+ * exists.
+ *
+ * @param id
+ * string record ID
+ * @return boolean indicating whether record exists locally
+ */
+ async itemExists(id) {
+ return id in (await this.getAllIDs());
+ },
+
+ /**
+ * Obtain the set of all known record IDs.
+ *
+ * @return Object with ID strings as keys and values of true. The values
+ * are ignored.
+ */
+ async getAllIDs() {
+ let ids = {};
+ for (let b of MailServices.ab.directories) {
+ if (
+ [
+ MailServices.ab.LDAP_DIRECTORY_TYPE,
+ MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ ].includes(b.dirType)
+ ) {
+ ids[b.UID] = true;
+ }
+ }
+ return ids;
+ },
+
+ /**
+ * Create a record from the specified ID.
+ *
+ * If the ID is known, the record should be populated with metadata from
+ * the store. If the ID is not known, the record should be created with the
+ * delete field set to true.
+ *
+ * @param id
+ * string record ID
+ * @param collection
+ * Collection to add record to. This is typically passed into the
+ * constructor for the newly-created record.
+ * @return record type for this engine
+ */
+ async createRecord(id, collection) {
+ let record = new AddressBookRecord(collection, id);
+
+ let book = MailServices.ab.getDirectoryFromUID(id);
+
+ // If we don't know about this ID, mark the record as deleted.
+ if (!book) {
+ record.deleted = true;
+ return record;
+ }
+
+ record.name = book.dirName;
+ record.type = book.dirType;
+ record.prefs = {};
+
+ function collectPrefs(prefData) {
+ for (let [key, realKey] of Object.entries(prefData)) {
+ realKey = `${book.dirPrefId}.${realKey}`;
+ switch (Services.prefs.getPrefType(realKey)) {
+ case Services.prefs.PREF_STRING:
+ record.prefs[key] = Services.prefs.getStringPref(realKey);
+ break;
+ case Services.prefs.PREF_INT:
+ record.prefs[key] = Services.prefs.getIntPref(realKey);
+ break;
+ case Services.prefs.PREF_BOOL:
+ record.prefs[key] = Services.prefs.getBoolPref(realKey);
+ break;
+ }
+ }
+ }
+
+ collectPrefs(SYNCED_COMMON_PROPERTIES);
+
+ if (book.dirType == MailServices.ab.CARDDAV_DIRECTORY_TYPE) {
+ collectPrefs(SYNCED_CARDDAV_PROPERTIES);
+ } else if (book.dirType == MailServices.ab.LDAP_DIRECTORY_TYPE) {
+ collectPrefs(SYNCED_LDAP_PROPERTIES);
+ }
+
+ return record;
+ },
+};
+
+function AddressBookTracker(name, engine) {
+ Tracker.call(this, name, engine);
+}
+AddressBookTracker.prototype = {
+ __proto__: Tracker.prototype,
+
+ _changedIDs: new Set(),
+ _ignoreAll: false,
+
+ async getChangedIDs() {
+ let changes = {};
+ for (let id of this._changedIDs) {
+ changes[id] = 0;
+ }
+ return changes;
+ },
+
+ clearChangedIDs() {
+ this._changedIDs.clear();
+ },
+
+ get ignoreAll() {
+ return this._ignoreAll;
+ },
+
+ set ignoreAll(value) {
+ this._ignoreAll = value;
+ },
+
+ onStart() {
+ Services.prefs.addObserver("ldap_2.servers.", this);
+ Services.obs.addObserver(this, "addrbook-directory-created");
+ Services.obs.addObserver(this, "addrbook-directory-deleted");
+ },
+
+ onStop() {
+ Services.prefs.removeObserver("ldap_2.servers.", this);
+ Services.obs.removeObserver(this, "addrbook-directory-created");
+ Services.obs.removeObserver(this, "addrbook-directory-deleted");
+ },
+
+ observe(subject, topic, data) {
+ if (this._ignoreAll) {
+ return;
+ }
+
+ let book;
+ switch (topic) {
+ case "nsPref:changed": {
+ let serverKey = data.split(".")[2];
+ let prefName = data.substring(serverKey.length + 16);
+ if (
+ prefName != "description" &&
+ !Object.values(SYNCED_COMMON_PROPERTIES).includes(prefName) &&
+ !Object.values(SYNCED_CARDDAV_PROPERTIES).includes(prefName) &&
+ !Object.values(SYNCED_LDAP_PROPERTIES).includes(prefName)
+ ) {
+ return;
+ }
+
+ book = MailServices.ab.getDirectoryFromId(
+ "ldap_2.servers." + serverKey
+ );
+ break;
+ }
+ case "addrbook-directory-created":
+ case "addrbook-directory-deleted":
+ book = subject;
+ break;
+ }
+
+ if (
+ book &&
+ [
+ MailServices.ab.LDAP_DIRECTORY_TYPE,
+ MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ ].includes(book.dirType) &&
+ !this._changedIDs.has(book.UID)
+ ) {
+ this._changedIDs.add(book.UID);
+ this.score += SCORE_INCREMENT_XLARGE;
+ }
+ },
+};
diff --git a/comm/mail/services/sync/modules/engines/calendars.sys.mjs b/comm/mail/services/sync/modules/engines/calendars.sys.mjs
new file mode 100644
index 0000000000..72a3799786
--- /dev/null
+++ b/comm/mail/services/sync/modules/engines/calendars.sys.mjs
@@ -0,0 +1,349 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
+import {
+ Store,
+ SyncEngine,
+ Tracker,
+} from "resource://services-sync/engines.sys.mjs";
+import { Utils } from "resource://services-sync/util.sys.mjs";
+
+const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
+ "resource://services-sync/constants.js"
+);
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+
+const SYNCED_PROPERTIES = {
+ cacheEnabled: "cache.enabled",
+ color: "color",
+ displayed: "calendar-main-in-composite",
+ disabled: "disabled",
+ forceEmailScheduling: "forceEmailScheduling",
+ // imipIdentityKey: "imip.identity.key",
+ readOnly: "readOnly",
+ refreshInterval: "refreshInterval",
+ sessionId: "sessionId",
+ suppressAlarms: "suppressAlarms",
+ username: "username",
+};
+
+function shouldSyncCalendar(calendar) {
+ if (calendar.type == "caldav") {
+ return true;
+ }
+ if (calendar.type == "ics") {
+ return calendar.uri.schemeIs("http") || calendar.uri.schemeIs("https");
+ }
+ return false;
+}
+
+/**
+ * CalendarRecord represents the state of an add-on in an application.
+ *
+ * Each add-on has its own record for each application ID it is installed
+ * on.
+ *
+ * The ID of add-on records is a randomly-generated GUID. It is random instead
+ * of deterministic so the URIs of the records cannot be guessed and so
+ * compromised server credentials won't result in disclosure of the specific
+ * add-ons present in a Sync account.
+ *
+ * The record contains the following fields:
+ *
+ */
+export function CalendarRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+
+CalendarRecord.prototype = {
+ __proto__: CryptoWrapper.prototype,
+ _logName: "Record.Calendar",
+};
+Utils.deferGetSet(CalendarRecord, "cleartext", [
+ "name",
+ "type",
+ "uri",
+ "prefs",
+]);
+
+export function CalendarsEngine(service) {
+ SyncEngine.call(this, "Calendars", service);
+}
+
+CalendarsEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: CalendarStore,
+ _trackerObj: CalendarTracker,
+ _recordObj: CalendarRecord,
+ version: 1,
+ syncPriority: 6,
+
+ /*
+ * Returns a changeset for this sync. Engine implementations can override this
+ * method to bypass the tracker for certain or all changed items.
+ */
+ async getChangedIDs() {
+ return this._tracker.getChangedIDs();
+ },
+};
+
+function CalendarStore(name, engine) {
+ Store.call(this, name, engine);
+}
+CalendarStore.prototype = {
+ __proto__: Store.prototype,
+
+ /**
+ * Create an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to create an item from
+ */
+ async create(record) {
+ if (!["caldav", "ics"].includes(record.type)) {
+ return;
+ }
+
+ let calendar = cal.manager.createCalendar(
+ record.type,
+ Services.io.newURI(record.uri)
+ );
+ calendar.name = record.name;
+
+ for (let [key, realKey] of Object.entries(SYNCED_PROPERTIES)) {
+ if (key in record.prefs) {
+ calendar.setProperty(realKey, record.prefs[key]);
+ }
+ }
+
+ // Set this *after* the properties so it can pick up the session ID or username.
+ calendar.id = record.id;
+ cal.manager.registerCalendar(calendar);
+ if (!calendar.getProperty("disabled")) {
+ calendar.refresh();
+ }
+ },
+
+ /**
+ * Remove an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to delete an item from
+ */
+ async remove(record) {
+ let calendar = cal.manager.getCalendarById(record.id);
+ if (!calendar) {
+ this._log.trace("Asked to remove record that doesn't exist, ignoring");
+ return;
+ }
+ cal.manager.removeCalendar(calendar);
+ },
+
+ /**
+ * Update an item from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The record to use to update an item from
+ */
+ async update(record) {
+ let calendar = cal.manager.getCalendarById(record.id);
+ if (!calendar) {
+ this._log.trace("Skipping update for unknown item: " + record.id);
+ return;
+ }
+ if (calendar.type != record.type) {
+ throw new Components.Exception(
+ `Refusing to change calendar type from ${calendar.type} to ${record.type}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ if (calendar.getProperty("cache.enabled") != record.prefs.cacheEnabled) {
+ throw new Components.Exception(
+ `Refusing to change the cache setting`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ calendar.name = record.name;
+ if (calendar.uri.spec != record.uri) {
+ calendar.uri = Services.io.newURI(record.uri); // Should this be allowed?
+ }
+ for (let [key, realKey] of Object.entries(SYNCED_PROPERTIES)) {
+ if (key in record.prefs) {
+ calendar.setProperty(realKey, record.prefs[key]);
+ } else if (calendar.getProperty(key)) {
+ // Only delete properties if they exist. Otherwise bad things happen.
+ calendar.deleteProperty(realKey);
+ }
+ }
+ },
+
+ /**
+ * Determine whether a record with the specified ID exists.
+ *
+ * Takes a string record ID and returns a booleans saying whether the record
+ * exists.
+ *
+ * @param id
+ * string record ID
+ * @return boolean indicating whether record exists locally
+ */
+ async itemExists(id) {
+ return id in (await this.getAllIDs());
+ },
+
+ /**
+ * Obtain the set of all known record IDs.
+ *
+ * @return Object with ID strings as keys and values of true. The values
+ * are ignored.
+ */
+ async getAllIDs() {
+ let ids = {};
+ for (let c of cal.manager.getCalendars()) {
+ if (shouldSyncCalendar(c)) {
+ ids[c.id] = true;
+ }
+ }
+ return ids;
+ },
+
+ /**
+ * Create a record from the specified ID.
+ *
+ * If the ID is known, the record should be populated with metadata from
+ * the store. If the ID is not known, the record should be created with the
+ * delete field set to true.
+ *
+ * @param id
+ * string record ID
+ * @param collection
+ * Collection to add record to. This is typically passed into the
+ * constructor for the newly-created record.
+ * @return record type for this engine
+ */
+ async createRecord(id, collection) {
+ let record = new CalendarRecord(collection, id);
+
+ let calendar = cal.manager.getCalendarById(id);
+
+ // If we don't know about this ID, mark the record as deleted.
+ if (!calendar) {
+ record.deleted = true;
+ return record;
+ }
+
+ record.name = calendar.name;
+ record.type = calendar.type;
+ record.uri = calendar.uri.spec;
+ record.prefs = {};
+
+ for (let [key, realKey] of Object.entries(SYNCED_PROPERTIES)) {
+ let value = calendar.getProperty(realKey);
+ if (value !== null) {
+ record.prefs[key] = value;
+ }
+ }
+
+ return record;
+ },
+};
+
+function CalendarTracker(name, engine) {
+ Tracker.call(this, name, engine);
+}
+CalendarTracker.prototype = {
+ __proto__: Tracker.prototype,
+
+ QueryInterface: cal.generateQI([
+ "calICalendarManagerObserver",
+ "nsIObserver",
+ ]),
+
+ _changedIDs: new Set(),
+ _ignoreAll: false,
+
+ async getChangedIDs() {
+ let changes = {};
+ for (let id of this._changedIDs) {
+ changes[id] = 0;
+ }
+ return changes;
+ },
+
+ clearChangedIDs() {
+ this._changedIDs.clear();
+ },
+
+ get ignoreAll() {
+ return this._ignoreAll;
+ },
+
+ set ignoreAll(value) {
+ this._ignoreAll = value;
+ },
+
+ onStart() {
+ Services.prefs.addObserver("calendar.registry.", this);
+ cal.manager.addObserver(this);
+ },
+
+ onStop() {
+ Services.prefs.removeObserver("calendar.registry.", this);
+ cal.manager.removeObserver(this);
+ },
+
+ observe(subject, topic, data) {
+ if (this._ignoreAll) {
+ return;
+ }
+
+ let id = data.split(".")[2];
+ let prefName = data.substring(id.length + 19);
+ if (
+ prefName != "name" &&
+ !Object.values(SYNCED_PROPERTIES).includes(prefName)
+ ) {
+ return;
+ }
+
+ let calendar = cal.manager.getCalendarById(id);
+ if (calendar && shouldSyncCalendar(calendar) && !this._changedIDs.has(id)) {
+ this._changedIDs.add(id);
+ this.score += SCORE_INCREMENT_XLARGE;
+ }
+ },
+
+ onCalendarRegistered(calendar) {
+ if (this._ignoreAll) {
+ return;
+ }
+
+ if (shouldSyncCalendar(calendar)) {
+ this._changedIDs.add(calendar.id);
+ this.score += SCORE_INCREMENT_XLARGE;
+ }
+ },
+ onCalendarUnregistering(calendar) {},
+ onCalendarDeleting(calendar) {
+ if (this._ignoreAll) {
+ return;
+ }
+
+ if (shouldSyncCalendar(calendar)) {
+ this._changedIDs.add(calendar.id);
+ this.score += SCORE_INCREMENT_XLARGE;
+ }
+ },
+};
diff --git a/comm/mail/services/sync/modules/engines/identities.sys.mjs b/comm/mail/services/sync/modules/engines/identities.sys.mjs
new file mode 100644
index 0000000000..07976272eb
--- /dev/null
+++ b/comm/mail/services/sync/modules/engines/identities.sys.mjs
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
+import {
+ Store,
+ SyncEngine,
+ Tracker,
+} from "resource://services-sync/engines.sys.mjs";
+import { Utils } from "resource://services-sync/util.sys.mjs";
+
+const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
+ "resource://services-sync/constants.js"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const SYNCED_IDENTITY_PROPERTIES = {
+ attachSignature: "attach_signature",
+ attachVCard: "attach_vcard",
+ autoQuote: "auto_quote",
+ catchAll: "catchAll",
+ catchAllHint: "catchAllHint",
+ composeHtml: "compose_html",
+ email: "useremail",
+ escapedVCard: "escapedVCard",
+ fullName: "fullName",
+ htmlSigFormat: "htmlSigFormat",
+ htmlSigText: "htmlSigText",
+ label: "label",
+ organization: "organization",
+ replyOnTop: "reply_on_top",
+ replyTo: "reply_to",
+ sigBottom: "sig_bottom",
+ sigOnForward: "sig_on_fwd",
+ sigOnReply: "sig_on_reply",
+};
+
+/**
+ * IdentityRecord represents the state of an add-on in an application.
+ *
+ * Each add-on has its own record for each application ID it is installed
+ * on.
+ *
+ * The ID of add-on records is a randomly-generated GUID. It is random instead
+ * of deterministic so the URIs of the records cannot be guessed and so
+ * compromised server credentials won't result in disclosure of the specific
+ * add-ons present in a Sync account.
+ *
+ * The record contains the following fields:
+ *
+ */
+export function IdentityRecord(collection, id) {
+ CryptoWrapper.call(this, collection, id);
+}
+
+IdentityRecord.prototype = {
+ __proto__: CryptoWrapper.prototype,
+ _logName: "Record.Identity",
+};
+Utils.deferGetSet(IdentityRecord, "cleartext", ["accounts", "prefs", "smtpID"]);
+
+export function IdentitiesEngine(service) {
+ SyncEngine.call(this, "Identities", service);
+}
+
+IdentitiesEngine.prototype = {
+ __proto__: SyncEngine.prototype,
+ _storeObj: IdentityStore,
+ _trackerObj: IdentityTracker,
+ _recordObj: IdentityRecord,
+ version: 1,
+ syncPriority: 4,
+
+ /*
+ * Returns a changeset for this sync. Engine implementations can override this
+ * method to bypass the tracker for certain or all changed items.
+ */
+ async getChangedIDs() {
+ return this._tracker.getChangedIDs();
+ },
+};
+
+function IdentityStore(name, engine) {
+ Store.call(this, name, engine);
+}
+IdentityStore.prototype = {
+ __proto__: Store.prototype,
+
+ /**
+ * Create an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to create an item from
+ */
+ async create(record) {
+ let identity = MailServices.accounts.createIdentity();
+ identity.UID = record.id;
+
+ for (let key of Object.keys(SYNCED_IDENTITY_PROPERTIES)) {
+ if (key in record.prefs) {
+ identity[key] = record.prefs[key];
+ }
+ }
+
+ if (record.smtpID) {
+ let smtpServer = MailServices.smtp.servers.find(
+ s => s.UID == record.smtpID
+ );
+ if (smtpServer) {
+ identity.smtpServerKey = smtpServer.key;
+ } else {
+ this._log.warn(
+ `Identity uses SMTP server ${record.smtpID}, but it doesn't exist.`
+ );
+ }
+ }
+
+ for (let { id, isDefault } of record.accounts) {
+ let account = MailServices.accounts.accounts.find(
+ a => a.incomingServer?.UID == id
+ );
+ if (account) {
+ account.addIdentity(identity);
+ if (isDefault) {
+ account.defaultIdentity = identity;
+ }
+ } else {
+ this._log.warn(`Identity is for account ${id}, but it doesn't exist.`);
+ }
+ }
+ },
+
+ /**
+ * Remove an item in the store from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The store record to delete an item from
+ */
+ async remove(record) {
+ let identity = MailServices.accounts.allIdentities.find(
+ i => i.UID == record.id
+ );
+ if (!identity) {
+ this._log.trace("Asked to remove record that doesn't exist, ignoring");
+ return;
+ }
+
+ for (let server of MailServices.accounts.getServersForIdentity(identity)) {
+ let account = MailServices.accounts.FindAccountForServer(server);
+ account.removeIdentity(identity);
+ // Removing the identity from one account should destroy it.
+ // No need to continue.
+ return;
+ }
+ },
+
+ /**
+ * Update an item from a record.
+ *
+ * This is called by the default implementation of applyIncoming(). If using
+ * applyIncomingBatch(), this won't be called unless your store calls it.
+ *
+ * @param record
+ * The record to use to update an item from
+ */
+ async update(record) {
+ let identity = MailServices.accounts.allIdentities.find(
+ i => i.UID == record.id
+ );
+ if (!identity) {
+ this._log.trace("Skipping update for unknown item: " + record.id);
+ return;
+ }
+
+ for (let key of Object.keys(SYNCED_IDENTITY_PROPERTIES)) {
+ if (key in record.prefs) {
+ identity[key] = record.prefs[key];
+ }
+ }
+
+ if (record.smtpID) {
+ let smtpServer = MailServices.smtp.servers.find(
+ s => s.UID == record.smtpID
+ );
+ if (smtpServer) {
+ identity.smtpServerKey = smtpServer.key;
+ } else {
+ this._log.warn(
+ `Identity uses SMTP server ${record.smtpID}, but it doesn't exist.`
+ );
+ }
+ } else {
+ identity.smtpServerKey = null;
+ }
+
+ for (let { id, isDefault } of record.accounts) {
+ let account = MailServices.accounts.accounts.find(
+ a => a.incomingServer?.UID == id
+ );
+ if (account) {
+ if (!account.identities.includes(identity)) {
+ account.addIdentity(identity);
+ }
+ if (isDefault && account.defaultIdentity != identity) {
+ account.defaultIdentity = identity;
+ }
+ } else {
+ this._log.warn(`Identity is for account ${id}, but it doesn't exist.`);
+ }
+ }
+ },
+
+ /**
+ * Determine whether a record with the specified ID exists.
+ *
+ * Takes a string record ID and returns a booleans saying whether the record
+ * exists.
+ *
+ * @param id
+ * string record ID
+ * @return boolean indicating whether record exists locally
+ */
+ async itemExists(id) {
+ return id in (await this.getAllIDs());
+ },
+
+ /**
+ * Obtain the set of all known record IDs.
+ *
+ * @return Object with ID strings as keys and values of true. The values
+ * are ignored.
+ */
+ async getAllIDs() {
+ let ids = {};
+ for (let i of MailServices.accounts.allIdentities) {
+ let servers = MailServices.accounts.getServersForIdentity(i);
+ if (servers.find(s => ["imap", "pop3"].includes(s.type))) {
+ ids[i.UID] = true;
+ }
+ }
+ return ids;
+ },
+
+ /**
+ * Create a record from the specified ID.
+ *
+ * If the ID is known, the record should be populated with metadata from
+ * the store. If the ID is not known, the record should be created with the
+ * delete field set to true.
+ *
+ * @param id
+ * string record ID
+ * @param collection
+ * Collection to add record to. This is typically passed into the
+ * constructor for the newly-created record.
+ * @return record type for this engine
+ */
+ async createRecord(id, collection) {
+ let record = new IdentityRecord(collection, id);
+
+ let identity = MailServices.accounts.allIdentities.find(i => i.UID == id);
+
+ // If we don't know about this ID, mark the record as deleted.
+ if (!identity) {
+ record.deleted = true;
+ return record;
+ }
+
+ record.accounts = [];
+ for (let server of MailServices.accounts.getServersForIdentity(identity)) {
+ let account = MailServices.accounts.FindAccountForServer(server);
+ if (account) {
+ record.accounts.push({
+ id: server.UID,
+ isDefault: account.defaultIdentity == identity,
+ });
+ }
+ }
+
+ record.prefs = {};
+ for (let key of Object.keys(SYNCED_IDENTITY_PROPERTIES)) {
+ record.prefs[key] = identity[key];
+ }
+
+ if (identity.smtpServerKey) {
+ let smtpServer = MailServices.smtp.getServerByIdentity(identity);
+ record.smtpID = smtpServer.UID;
+ }
+
+ return record;
+ },
+};
+
+function IdentityTracker(name, engine) {
+ Tracker.call(this, name, engine);
+}
+IdentityTracker.prototype = {
+ __proto__: Tracker.prototype,
+
+ _changedIDs: new Set(),
+ _ignoreAll: false,
+
+ async getChangedIDs() {
+ let changes = {};
+ for (let id of this._changedIDs) {
+ changes[id] = 0;
+ }
+ return changes;
+ },
+
+ clearChangedIDs() {
+ this._changedIDs.clear();
+ },
+
+ get ignoreAll() {
+ return this._ignoreAll;
+ },
+
+ set ignoreAll(value) {
+ this._ignoreAll = value;
+ },
+
+ onStart() {
+ Services.prefs.addObserver("mail.identity.", this);
+ Services.obs.addObserver(this, "account-identity-added");
+ Services.obs.addObserver(this, "account-identity-removed");
+ Services.obs.addObserver(this, "account-default-identity-changed");
+ },
+
+ onStop() {
+ Services.prefs.removeObserver("mail.account.", this);
+ Services.obs.removeObserver(this, "account-identity-added");
+ Services.obs.removeObserver(this, "account-identity-removed");
+ Services.obs.removeObserver(this, "account-default-identity-changed");
+ },
+
+ observe(subject, topic, data) {
+ if (this._ignoreAll) {
+ return;
+ }
+
+ let markAsChanged = identity => {
+ if (identity && !this._changedIDs.has(identity.UID)) {
+ this._changedIDs.add(identity.UID);
+ this.score = SCORE_INCREMENT_XLARGE;
+ }
+ };
+
+ if (
+ ["account-identity-added", "account-identity-removed"].includes(topic)
+ ) {
+ markAsChanged(subject.QueryInterface(Ci.nsIMsgIdentity));
+ return;
+ }
+
+ if (topic == "account-default-identity-changed") {
+ // The default identity has changed, update the default identity and
+ // the previous one, which will now be second on the list.
+ let [newDefault, oldDefault] = Services.prefs
+ .getStringPref(`mail.account.${data}.identities`)
+ .split(",");
+ if (newDefault) {
+ markAsChanged(MailServices.accounts.getIdentity(newDefault));
+ }
+ if (oldDefault) {
+ markAsChanged(MailServices.accounts.getIdentity(oldDefault));
+ }
+ return;
+ }
+
+ let idKey = data.split(".")[2];
+ let prefName = data.substring(idKey.length + 15);
+ if (
+ prefName != "smtpServer" &&
+ !Object.values(SYNCED_IDENTITY_PROPERTIES).includes(prefName)
+ ) {
+ return;
+ }
+
+ // Don't use .getIdentity because it will create one if it doesn't exist.
+ markAsChanged(
+ MailServices.accounts.allIdentities.find(i => i.key == idKey)
+ );
+ },
+};
diff --git a/comm/mail/services/sync/moz.build b/comm/mail/services/sync/moz.build
new file mode 100644
index 0000000000..47321a80c6
--- /dev/null
+++ b/comm/mail/services/sync/moz.build
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES["services-sync"].engines += [
+ "modules/engines/accounts.sys.mjs",
+ "modules/engines/addressBooks.sys.mjs",
+ "modules/engines/calendars.sys.mjs",
+ "modules/engines/identities.sys.mjs",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "test/unit/xpcshell.ini",
+]
diff --git a/comm/mail/services/sync/test/unit/head.js b/comm/mail/services/sync/test/unit/head.js
new file mode 100644
index 0000000000..8ccbe36050
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/head.js
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function newUID() {
+ return Services.uuid.generateUUID().toString().substring(1, 37);
+}
diff --git a/comm/mail/services/sync/test/unit/test_account_store.js b/comm/mail/services/sync/test/unit/test_account_store.js
new file mode 100644
index 0000000000..e920ce14fe
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_account_store.js
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { AccountsEngine, AccountRecord } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/accounts.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+let engine, store, tracker;
+let imapAccount, imapServer, pop3Account, pop3Server, smtpServer;
+
+add_setup(async function () {
+ engine = new AccountsEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+
+ try {
+ // Ensure there is a local mail account...
+ MailServices.accounts.localFoldersServer;
+ } catch {
+ // ... if not, make one.
+ MailServices.accounts.createLocalMailAccount();
+ }
+
+ imapAccount = MailServices.accounts.createAccount();
+ imapServer = imapAccount.incomingServer =
+ MailServices.accounts.createIncomingServer("username", "hostname", "imap");
+ imapAccount.incomingServer.prettyName = "IMAP Server";
+
+ Assert.ok(imapServer.UID);
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${imapServer.key}.uid`),
+ imapServer.UID
+ );
+
+ pop3Account = MailServices.accounts.createAccount();
+ pop3Server = pop3Account.incomingServer =
+ MailServices.accounts.createIncomingServer("username", "hostname", "pop3");
+ pop3Account.incomingServer.prettyName = "POP3 Server";
+
+ Assert.ok(pop3Server.UID);
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${pop3Server.key}.uid`),
+ pop3Server.UID
+ );
+
+ smtpServer = MailServices.smtp.createServer();
+ smtpServer.username = "username";
+ smtpServer.hostname = "hostname";
+ smtpServer.description = "SMTP Server";
+
+ Assert.ok(smtpServer.UID);
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.smtpserver.${smtpServer.key}.uid`, ""),
+ smtpServer.UID
+ );
+
+ // Sanity check.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(MailServices.smtp.servers.length, 1);
+});
+
+add_task(async function testGetAllIDs() {
+ Assert.deepEqual(await store.getAllIDs(), {
+ [imapServer.UID]: true,
+ [pop3Server.UID]: true,
+ [smtpServer.UID]: true,
+ });
+});
+
+add_task(async function testItemExists() {
+ Assert.equal(await store.itemExists(imapServer.UID), true);
+ Assert.equal(await store.itemExists(pop3Server.UID), true);
+ Assert.equal(await store.itemExists(smtpServer.UID), true);
+});
+
+add_task(async function testCreateIMAPRecord() {
+ let record = await store.createRecord(imapServer.UID);
+ Assert.ok(record instanceof AccountRecord);
+ Assert.equal(record.id, imapServer.UID);
+ Assert.equal(record.username, "username");
+ Assert.equal(record.hostname, "hostname");
+ Assert.equal(record.type, "imap");
+ Assert.deepEqual(record.prefs, {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 143,
+ prettyName: "IMAP Server",
+ socketType: 0,
+ });
+ Assert.equal(record.isDefault, undefined);
+});
+
+add_task(async function testCreatePOP3Record() {
+ let record = await store.createRecord(pop3Server.UID);
+ Assert.ok(record instanceof AccountRecord);
+ Assert.equal(record.id, pop3Server.UID);
+ Assert.equal(record.username, "username");
+ Assert.equal(record.hostname, "hostname");
+ Assert.equal(record.type, "pop3");
+ Assert.deepEqual(record.prefs, {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 110,
+ prettyName: "POP3 Server",
+ socketType: 0,
+ });
+ Assert.equal(record.isDefault, undefined);
+});
+
+add_task(async function testCreateSMTPRecord() {
+ let smtpServerID = smtpServer.UID;
+
+ let record = await store.createRecord(smtpServerID);
+ Assert.ok(record instanceof AccountRecord);
+ Assert.equal(record.id, smtpServerID);
+ Assert.equal(record.username, "username");
+ Assert.equal(record.hostname, "hostname");
+ Assert.equal(record.type, "smtp");
+ Assert.deepEqual(record.prefs, {
+ authMethod: 3,
+ port: 0,
+ description: "SMTP Server",
+ socketType: 0,
+ });
+ Assert.equal(record.isDefault, true);
+});
+
+add_task(async function testCreateDeletedRecord() {
+ let fakeID = "12345678-1234-1234-1234-123456789012";
+ let record = await store.createRecord(fakeID);
+ Assert.ok(record instanceof AccountRecord);
+ Assert.equal(record.id, fakeID);
+ Assert.equal(record.deleted, true);
+});
+
+add_task(async function testSyncIMAPRecords() {
+ let newID = newUID();
+ await store.applyIncoming({
+ id: newID,
+ username: "username",
+ hostname: "new.hostname",
+ type: "imap",
+ prefs: {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 143,
+ prettyName: "New IMAP Server",
+ socketType: Ci.nsMsgSocketType.plain,
+ },
+ });
+
+ Assert.equal(MailServices.accounts.accounts.length, 4);
+
+ let newServer = MailServices.accounts.allServers.find(s => s.UID == newID);
+ Assert.equal(newServer.username, "username");
+ Assert.equal(newServer.hostName, "new.hostname");
+ Assert.equal(newServer.prettyName, "New IMAP Server");
+ Assert.equal(newServer.port, 143);
+ Assert.equal(newServer.socketType, Ci.nsMsgSocketType.plain);
+
+ await store.applyIncoming({
+ id: newID,
+ username: "username",
+ hostname: "new.hostname",
+ type: "imap",
+ prefs: {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 993,
+ prettyName: "Changed IMAP Server",
+ socketType: Ci.nsMsgSocketType.SSL,
+ },
+ });
+
+ Assert.equal(newServer.prettyName, "Changed IMAP Server");
+ Assert.equal(newServer.port, 993);
+ Assert.equal(newServer.socketType, Ci.nsMsgSocketType.SSL);
+
+ await Assert.rejects(
+ store.applyIncoming({
+ id: newID,
+ type: "pop3",
+ }),
+ /Refusing to change server type/
+ );
+
+ await store.applyIncoming({
+ id: newID,
+ deleted: true,
+ });
+
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+});
+
+add_task(async function testSyncPOP3Records() {
+ let newID = newUID();
+ await store.applyIncoming({
+ id: newID,
+ username: "username",
+ hostname: "new.hostname",
+ type: "pop3",
+ prefs: {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 110,
+ prettyName: "New POP3 Server",
+ socketType: Ci.nsMsgSocketType.plain,
+ },
+ });
+
+ Assert.equal(MailServices.accounts.accounts.length, 4);
+
+ let newServer = MailServices.accounts.allServers.find(s => s.UID == newID);
+ Assert.equal(newServer.username, "username");
+ Assert.equal(newServer.hostName, "new.hostname");
+ Assert.equal(newServer.prettyName, "New POP3 Server");
+ Assert.equal(newServer.port, 110);
+ Assert.equal(newServer.socketType, Ci.nsMsgSocketType.plain);
+
+ await store.applyIncoming({
+ id: newID,
+ username: "username",
+ hostname: "new.hostname",
+ type: "pop3",
+ prefs: {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 995,
+ prettyName: "Changed POP3 Server",
+ socketType: Ci.nsMsgSocketType.SSL,
+ },
+ });
+
+ Assert.equal(newServer.prettyName, "Changed POP3 Server");
+ Assert.equal(newServer.port, 995);
+ Assert.equal(newServer.socketType, Ci.nsMsgSocketType.SSL);
+
+ await Assert.rejects(
+ store.applyIncoming({
+ id: newID,
+ type: "imap",
+ }),
+ /Refusing to change server type/
+ );
+
+ await store.applyIncoming({
+ id: newID,
+ deleted: true,
+ });
+
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+});
+
+add_task(async function testSyncSMTPRecords() {
+ let newSMTPServerID = newUID();
+ await store.applyIncoming({
+ id: newSMTPServerID,
+ username: "username",
+ hostname: "hostname",
+ type: "smtp",
+ prefs: {
+ authMethod: 3,
+ port: 0,
+ description: "Second Outgoing Server",
+ socketType: 0,
+ },
+ isDefault: true,
+ });
+
+ Assert.equal(MailServices.smtp.servers.length, 2);
+
+ let newSMTPServer = MailServices.smtp.servers.find(
+ s => s.UID == newSMTPServerID
+ );
+ Assert.equal(newSMTPServer.username, "username");
+ Assert.equal(newSMTPServer.hostname, "hostname");
+ Assert.equal(newSMTPServer.description, "Second Outgoing Server");
+ Assert.equal(MailServices.smtp.defaultServer.key, newSMTPServer.key);
+
+ await store.applyIncoming({
+ id: smtpServer.UID,
+ username: "username",
+ hostname: "new.hostname",
+ type: "smtp",
+ prefs: {
+ authMethod: 3,
+ port: 0,
+ description: "New SMTP Server",
+ socketType: 0,
+ },
+ isDefault: true,
+ });
+
+ Assert.equal(smtpServer.description, "New SMTP Server");
+ Assert.equal(MailServices.smtp.defaultServer.key, smtpServer.key);
+
+ // TODO test update
+
+ await store.applyIncoming({
+ id: newSMTPServerID,
+ deleted: true,
+ });
+
+ Assert.equal(MailServices.smtp.servers.length, 1);
+ Assert.equal(MailServices.smtp.servers[0].key, smtpServer.key);
+ Assert.equal(MailServices.smtp.defaultServer.key, smtpServer.key);
+});
diff --git a/comm/mail/services/sync/test/unit/test_account_tracker.js b/comm/mail/services/sync/test/unit/test_account_tracker.js
new file mode 100644
index 0000000000..98a132b48f
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_account_tracker.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { AccountsEngine } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/accounts.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+let engine, store, tracker;
+
+add_setup(async function () {
+ engine = new AccountsEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+ tracker = engine._tracker;
+
+ Assert.equal(tracker.score, 0);
+ Assert.equal(tracker._isTracking, false);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ try {
+ // Ensure there is a local mail account...
+ MailServices.accounts.localFoldersServer;
+ } catch {
+ // ... if not, make one.
+ MailServices.accounts.createLocalMailAccount();
+ }
+
+ tracker.start();
+ Assert.equal(tracker._isTracking, true);
+});
+
+/**
+ * Test creating, changing, and deleting an account that should be synced.
+ */
+add_task(async function testAccount() {
+ Assert.equal(tracker.score, 0);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ let id = newUID();
+ info(id);
+ let newAccount = MailServices.accounts.createAccount();
+ newAccount.incomingServer = MailServices.accounts.createIncomingServer(
+ "username",
+ "hostname",
+ "imap"
+ );
+ newAccount.incomingServer.UID = id;
+ newAccount.incomingServer.prettyName = "First Incoming Server";
+
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ tracker.resetScore();
+ Assert.equal(tracker.score, 0);
+
+ newAccount.incomingServer.prettyName = "Changed name";
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ MailServices.accounts.removeAccount(newAccount, true);
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+});
+
+/**
+ * Test the store methods on calendars. The tracker should ignore them.
+ */
+add_task(async function testIncomingChanges() {
+ let id = newUID();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ username: "username",
+ hostname: "new.hostname",
+ type: "imap",
+ prefs: {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 143,
+ prettyName: "New IMAP Server",
+ socketType: Ci.nsMsgSocketType.plain,
+ },
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ username: "username",
+ hostname: "new.hostname",
+ type: "imap",
+ prefs: {
+ authMethod: 3,
+ biffMinutes: 10,
+ doBiff: true,
+ downloadOnBiff: false,
+ emptyTrashOnExit: false,
+ incomingDuplicateAction: 0,
+ limitOfflineMessageSize: false,
+ loginAtStartUp: false,
+ maxMessageSize: 50,
+ port: 993,
+ prettyName: "Changed IMAP Server",
+ socketType: Ci.nsMsgSocketType.SSL,
+ },
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ deleted: true,
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+});
diff --git a/comm/mail/services/sync/test/unit/test_addressBook_store.js b/comm/mail/services/sync/test/unit/test_addressBook_store.js
new file mode 100644
index 0000000000..d32b80eac6
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_addressBook_store.js
@@ -0,0 +1,130 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { AddressBooksEngine, AddressBookRecord } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/addressBooks.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+let engine, store, tracker;
+let cardDAVBook;
+
+// TODO test ldap books
+
+add_setup(async function () {
+ engine = new AddressBooksEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "Sync Address Book",
+ null,
+ MailServices.ab.CARDDAV_DIRECTORY_TYPE
+ );
+ cardDAVBook = MailServices.ab.getDirectoryFromId(dirPrefId);
+ cardDAVBook.setStringValue("carddav.url", "https://localhost:1234/a/book");
+});
+
+add_task(async function testGetAllIDs() {
+ Assert.deepEqual(await store.getAllIDs(), {
+ [cardDAVBook.UID]: true,
+ });
+});
+
+add_task(async function testItemExists() {
+ Assert.equal(await store.itemExists(cardDAVBook.UID), true);
+});
+
+add_task(async function testCreateRecord() {
+ let record = await store.createRecord(cardDAVBook.UID);
+ Assert.ok(record instanceof AddressBookRecord);
+ Assert.equal(record.id, cardDAVBook.UID);
+ Assert.equal(record.name, "Sync Address Book");
+ Assert.equal(record.type, MailServices.ab.CARDDAV_DIRECTORY_TYPE);
+ Assert.deepEqual(record.prefs, { url: "https://localhost:1234/a/book" });
+});
+
+add_task(async function testCreateDeletedRecord() {
+ let fakeID = "12345678-1234-1234-1234-123456789012";
+ let record = await store.createRecord(fakeID);
+ Assert.ok(record instanceof AddressBookRecord);
+ Assert.equal(record.id, fakeID);
+ Assert.equal(record.deleted, true);
+});
+
+add_task(async function testSyncRecords() {
+ Assert.equal(MailServices.ab.directories.length, 3);
+ PromiseTestUtils.expectUncaughtRejection(/Connection failure/);
+
+ let newID = newUID();
+ await store.applyIncoming({
+ id: newID,
+ name: "bar",
+ type: MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ prefs: {
+ url: "https://localhost/",
+ syncInterval: 0,
+ username: "username",
+ },
+ });
+ Services.obs.notifyObservers(null, "weave:service:sync:finish");
+
+ Assert.equal(MailServices.ab.directories.length, 4);
+ let newBook = MailServices.ab.getDirectoryFromUID(newID);
+ Assert.equal(newBook.dirName, "bar");
+ Assert.equal(newBook.dirType, MailServices.ab.CARDDAV_DIRECTORY_TYPE);
+ Assert.equal(
+ newBook.getStringValue("carddav.url", null),
+ "https://localhost/"
+ );
+ Assert.equal(newBook.getIntValue("carddav.syncinterval", null), 0);
+ Assert.equal(newBook.getStringValue("carddav.username", null), "username");
+
+ await store.applyIncoming({
+ id: newID,
+ name: "bar!",
+ type: MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ prefs: {
+ url: "https://localhost/",
+ syncInterval: 30,
+ username: "username@localhost",
+ },
+ });
+
+ Assert.equal(MailServices.ab.directories.length, 4);
+ newBook = MailServices.ab.getDirectoryFromUID(newID);
+ Assert.equal(newBook.dirName, "bar!");
+ Assert.equal(newBook.dirType, MailServices.ab.CARDDAV_DIRECTORY_TYPE);
+ Assert.equal(
+ newBook.getStringValue("carddav.url", null),
+ "https://localhost/"
+ );
+ Assert.equal(newBook.getIntValue("carddav.syncinterval", null), 30);
+ Assert.equal(
+ newBook.getStringValue("carddav.username", null),
+ "username@localhost"
+ );
+
+ await store.applyIncoming({
+ id: newID,
+ deleted: true,
+ });
+
+ Assert.equal(MailServices.ab.directories.length, 3);
+ newBook = MailServices.ab.getDirectoryFromUID(newID);
+ Assert.equal(newBook, null);
+});
diff --git a/comm/mail/services/sync/test/unit/test_addressBook_tracker.js b/comm/mail/services/sync/test/unit/test_addressBook_tracker.js
new file mode 100644
index 0000000000..9831252fb6
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_addressBook_tracker.js
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { AddressBooksEngine } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/addressBooks.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromiseTestUtils.sys.mjs"
+);
+
+let engine, store, tracker;
+
+add_setup(async function () {
+ engine = new AddressBooksEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+ tracker = engine._tracker;
+
+ Assert.equal(tracker.score, 0);
+ Assert.equal(tracker._isTracking, false);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ tracker.start();
+ Assert.equal(tracker._isTracking, true);
+});
+
+/**
+ * Test creating, changing, and deleting an address book that should be synced.
+ */
+add_task(async function testNetworkAddressBook() {
+ Assert.equal(tracker.score, 0);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ let id = newUID();
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "Sync Address Book",
+ null,
+ MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ id
+ );
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ tracker.resetScore();
+ Assert.equal(tracker.score, 0);
+
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ book.dirName = "changed name";
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ book.setIntValue("carddav.syncinterval", 0);
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ book.setStringValue("carddav.url", "https://localhost/");
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ let deletedPromise = TestUtils.topicObserved("addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(book.URI);
+ await deletedPromise;
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+});
+
+/**
+ * Test a local address book. This shouldn't affect the tracker at all.
+ */
+add_task(async function testStorageAddressBook() {
+ let dirPrefId = MailServices.ab.newAddressBook(
+ "Sync Address Book",
+ null,
+ MailServices.ab.JS_DIRECTORY_TYPE
+ );
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ let book = MailServices.ab.getDirectoryFromId(dirPrefId);
+ book.dirName = "changed name";
+ book.setBoolValue("readOnly", true);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ let deletedPromise = TestUtils.topicObserved("addrbook-directory-deleted");
+ MailServices.ab.deleteAddressBook(book.URI);
+ await deletedPromise;
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+});
+
+/**
+ * Test the store methods on address books. The tracker should ignore them.
+ */
+add_task(async function testIncomingChanges() {
+ PromiseTestUtils.expectUncaughtRejection(/Connection failure/);
+
+ let id = newUID();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ name: "New Book",
+ type: MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ prefs: {
+ url: "https://localhost/",
+ syncInterval: 0,
+ username: "username",
+ },
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ name: "New Book (changed)!",
+ type: MailServices.ab.CARDDAV_DIRECTORY_TYPE,
+ prefs: {
+ url: "https://localhost/",
+ syncInterval: 30,
+ username: "username@localhost",
+ },
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ deleted: true,
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+});
diff --git a/comm/mail/services/sync/test/unit/test_calendar_store.js b/comm/mail/services/sync/test/unit/test_calendar_store.js
new file mode 100644
index 0000000000..41dd01c22a
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_calendar_store.js
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { CalendarsEngine, CalendarRecord } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/calendars.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+
+let engine, store, tracker;
+let calDAVCalendar, icsCalendar, fileICSCalendar, storageCalendar;
+
+// TODO test caldav calendars
+
+add_setup(async function () {
+ await new Promise(resolve => cal.manager.startup({ onResult: resolve }));
+
+ engine = new CalendarsEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+
+ calDAVCalendar = cal.manager.createCalendar(
+ "caldav",
+ Services.io.newURI("https://localhost/caldav")
+ );
+ calDAVCalendar.name = "CalDAV Calendar";
+ cal.manager.registerCalendar(calDAVCalendar);
+
+ icsCalendar = cal.manager.createCalendar(
+ "ics",
+ Services.io.newURI("https://localhost/ics")
+ );
+ icsCalendar.name = "ICS Calendar";
+ cal.manager.registerCalendar(icsCalendar);
+
+ fileICSCalendar = cal.manager.createCalendar(
+ "ics",
+ Services.io.newURI("file:///home/user/test.ics")
+ );
+ fileICSCalendar.name = "File ICS Calendar";
+ cal.manager.registerCalendar(fileICSCalendar);
+
+ storageCalendar = cal.manager.createCalendar(
+ "storage",
+ Services.io.newURI("moz-storage-calendar://")
+ );
+ storageCalendar.name = "Storage Calendar";
+ cal.manager.registerCalendar(storageCalendar);
+});
+
+add_task(async function testGetAllIDs() {
+ Assert.deepEqual(await store.getAllIDs(), {
+ [calDAVCalendar.id]: true,
+ [icsCalendar.id]: true,
+ });
+});
+
+add_task(async function testItemExists() {
+ Assert.equal(await store.itemExists(calDAVCalendar.id), true);
+ Assert.equal(await store.itemExists(icsCalendar.id), true);
+});
+
+add_task(async function testCreateCalDAVRecord() {
+ let record = await store.createRecord(calDAVCalendar.id);
+ Assert.ok(record instanceof CalendarRecord);
+ Assert.equal(record.id, calDAVCalendar.id);
+ Assert.equal(record.name, "CalDAV Calendar");
+ Assert.equal(record.type, "caldav");
+ Assert.equal(record.uri, "https://localhost/caldav");
+ Assert.deepEqual(record.prefs, {});
+});
+
+add_task(async function testCreateICSRecord() {
+ let record = await store.createRecord(icsCalendar.id);
+ Assert.ok(record instanceof CalendarRecord);
+ Assert.equal(record.id, icsCalendar.id);
+ Assert.equal(record.name, "ICS Calendar");
+ Assert.equal(record.type, "ics");
+ Assert.equal(record.uri, "https://localhost/ics");
+ Assert.deepEqual(record.prefs, {});
+});
+
+add_task(async function testCreateDeletedRecord() {
+ let fakeID = "12345678-1234-1234-1234-123456789012";
+ let record = await store.createRecord(fakeID);
+ Assert.ok(record instanceof CalendarRecord);
+ Assert.equal(record.id, fakeID);
+ Assert.equal(record.deleted, true);
+});
+
+add_task(async function testSyncRecords() {
+ // Sync a new calendar.
+
+ let newID = newUID();
+ await store.applyIncoming({
+ id: newID,
+ name: "New ICS Calendar",
+ type: "ics",
+ uri: "https://localhost/newICS",
+ prefs: {
+ color: "#abcdef",
+ },
+ });
+
+ Assert.equal(cal.manager.getCalendars().length, 5);
+ let calendar = cal.manager.getCalendarById(newID);
+ Assert.equal(calendar.id, newID);
+ Assert.equal(calendar.name, "New ICS Calendar");
+ Assert.equal(calendar.type, "ics");
+ Assert.equal(calendar.uri.spec, "https://localhost/newICS");
+ Assert.equal(calendar.getProperty("color"), "#abcdef");
+
+ // Change the name and some properties.
+
+ await store.applyIncoming({
+ id: newID,
+ name: "Changed ICS Calendar",
+ type: "ics",
+ uri: "https://localhost/changedICS",
+ prefs: {
+ color: "#123456",
+ readOnly: true,
+ },
+ });
+
+ Assert.equal(cal.manager.getCalendars().length, 5);
+ calendar = cal.manager.getCalendarById(newID);
+ Assert.equal(calendar.name, "Changed ICS Calendar");
+ Assert.equal(calendar.type, "ics");
+ Assert.equal(calendar.uri.spec, "https://localhost/changedICS");
+ Assert.equal(calendar.getProperty("color"), "#123456");
+ Assert.equal(calendar.getProperty("readOnly"), true);
+
+ // Change the calendar type. This should fail.
+
+ await Assert.rejects(
+ store.applyIncoming({
+ id: newID,
+ name: "New CalDAV Calendar",
+ type: "caldav",
+ uri: "https://localhost/caldav",
+ prefs: {
+ color: "#123456",
+ readOnly: true,
+ },
+ }),
+ /Refusing to change calendar type/
+ );
+
+ // Enable the cache. This should fail.
+
+ await Assert.rejects(
+ store.applyIncoming({
+ id: newID,
+ name: "Changed ICS Calendar",
+ type: "ics",
+ uri: "https://localhost/changedICS",
+ prefs: {
+ cacheEnabled: true,
+ color: "#123456",
+ readOnly: true,
+ },
+ }),
+ /Refusing to change the cache setting/
+ );
+
+ await store.applyIncoming({
+ id: newID,
+ deleted: true,
+ });
+
+ Assert.equal(cal.manager.getCalendars().length, 4);
+ calendar = cal.manager.getCalendarById(newID);
+ Assert.equal(calendar, null);
+});
diff --git a/comm/mail/services/sync/test/unit/test_calendar_tracker.js b/comm/mail/services/sync/test/unit/test_calendar_tracker.js
new file mode 100644
index 0000000000..042f5782aa
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_calendar_tracker.js
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
+const { CalendarsEngine } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/calendars.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+
+let engine, store, tracker;
+
+add_setup(async function () {
+ await new Promise(resolve => cal.manager.startup({ onResult: resolve }));
+ cal.manager.getCalendars();
+
+ engine = new CalendarsEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+ tracker = engine._tracker;
+
+ Assert.equal(tracker.score, 0);
+ Assert.equal(tracker._isTracking, false);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ tracker.start();
+ Assert.equal(tracker._isTracking, true);
+});
+
+/**
+ * Test creating, changing, and deleting a calendar that should be synced.
+ */
+add_task(async function testNetworkCalendar() {
+ Assert.equal(tracker.score, 0);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ let id = newUID();
+ let calendar = cal.manager.createCalendar(
+ "ics",
+ Services.io.newURI("https://localhost:1234/a/calendar")
+ );
+ calendar.name = "Sync Calendar";
+ calendar.id = id;
+ cal.manager.registerCalendar(calendar);
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ tracker.resetScore();
+ Assert.equal(tracker.score, 0);
+
+ calendar.name = "changed name";
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ calendar.setProperty("color", "#123456");
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ calendar.setProperty("calendar-main-in-composite", true);
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ cal.manager.unregisterCalendar(calendar);
+ cal.manager.removeCalendar(calendar);
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+});
+
+/**
+ * Test a storage calendar. This shouldn't affect the tracker at all.
+ */
+add_task(async function testStorageCalendar() {
+ let storageCalendar = cal.manager.createCalendar(
+ "storage",
+ Services.io.newURI("moz-storage-calendar://")
+ );
+ storageCalendar.name = "Sync Calendar";
+ storageCalendar.id = newUID();
+ cal.manager.registerCalendar(storageCalendar);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ storageCalendar.name = "changed name";
+ storageCalendar.setProperty("color", "#123456");
+ storageCalendar.setProperty("calendar-main-in-composite", true);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ cal.manager.unregisterCalendar(storageCalendar);
+ cal.manager.removeCalendar(storageCalendar);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+});
+
+/**
+ * Test the store methods on calendars. The tracker should ignore them.
+ */
+add_task(async function testIncomingChanges() {
+ let id = newUID();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ name: "New Calendar",
+ type: "ics",
+ uri: "https://localhost/ics",
+ prefs: {},
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ name: "New Calendar (changed)",
+ type: "ics",
+ uri: "https://localhost/ics",
+ prefs: {},
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ deleted: true,
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+});
diff --git a/comm/mail/services/sync/test/unit/test_identity_store.js b/comm/mail/services/sync/test/unit/test_identity_store.js
new file mode 100644
index 0000000000..42d4a0f356
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_identity_store.js
@@ -0,0 +1,222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { IdentitiesEngine, IdentityRecord } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/identities.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+let engine, store, tracker;
+let accountA, smtpServerA, identityA;
+let accountB, identityB;
+
+add_setup(async function () {
+ engine = new IdentitiesEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+
+ try {
+ // Ensure there is a local mail account...
+ MailServices.accounts.localFoldersServer;
+ } catch {
+ // ... if not, make one.
+ MailServices.accounts.createLocalMailAccount();
+ }
+
+ // Mail account and identity.
+
+ accountA = MailServices.accounts.createAccount();
+ accountA.incomingServer = MailServices.accounts.createIncomingServer(
+ "username",
+ "hostname",
+ "imap"
+ );
+ smtpServerA = MailServices.smtp.createServer();
+
+ identityA = MailServices.accounts.createIdentity();
+ identityA.email = "username@hostname";
+ identityA.fullName = "User";
+ identityA.smtpServerKey = smtpServerA.key;
+ accountA.addIdentity(identityA);
+
+ Assert.ok(identityA.UID);
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityA.key}.uid`),
+ identityA.UID
+ );
+
+ // NNTP account and identity. NNTP isn't currently supported, so this test
+ // will prove the identity isn't synced.
+
+ accountB = MailServices.accounts.createAccount();
+ accountB.incomingServer = MailServices.accounts.createIncomingServer(
+ "username",
+ "hostname",
+ "nntp"
+ );
+
+ identityB = MailServices.accounts.createIdentity();
+ identityB.email = "username@hostname";
+ identityB.fullName = "user";
+ accountB.addIdentity(identityB);
+
+ // Sanity check.
+
+ Assert.equal(MailServices.accounts.allIdentities.length, 2);
+ Assert.equal(accountA.identities.length, 1);
+ Assert.equal(accountA.defaultIdentity.key, identityA.key);
+ Assert.equal(accountB.identities.length, 1);
+ Assert.equal(accountB.defaultIdentity.key, identityB.key);
+});
+
+add_task(async function testGetAllIDs() {
+ Assert.deepEqual(await store.getAllIDs(), {
+ [identityA.UID]: true,
+ });
+});
+
+add_task(async function testItemExists() {
+ Assert.equal(await store.itemExists(identityA.UID), true);
+});
+
+add_task(async function testCreateRecord() {
+ let record = await store.createRecord(identityA.UID);
+ Assert.ok(record instanceof IdentityRecord);
+ Assert.equal(record.id, identityA.UID);
+ Assert.deepEqual(record.accounts, [
+ {
+ id: accountA.incomingServer.UID,
+ isDefault: true,
+ },
+ ]);
+ Assert.deepEqual(record.prefs, {
+ attachSignature: false,
+ attachVCard: false,
+ autoQuote: true,
+ catchAll: false,
+ catchAllHint: null,
+ composeHtml: true,
+ email: "username@hostname",
+ escapedVCard: null,
+ fullName: "User",
+ htmlSigFormat: false,
+ htmlSigText: "",
+ label: "",
+ organization: "",
+ replyOnTop: 0,
+ replyTo: null,
+ sigBottom: true,
+ sigOnForward: false,
+ sigOnReply: true,
+ });
+ Assert.equal(record.smtpID, smtpServerA.UID);
+});
+
+add_task(async function testCreateDeletedRecord() {
+ let fakeID = "12345678-1234-1234-1234-123456789012";
+ let record = await store.createRecord(fakeID);
+ Assert.ok(record instanceof IdentityRecord);
+ Assert.equal(record.id, fakeID);
+ Assert.equal(record.deleted, true);
+});
+
+add_task(async function testSyncRecords() {
+ let newIdentityID = newUID();
+ await store.applyIncoming({
+ id: newIdentityID,
+ accounts: [
+ {
+ id: accountA.incomingServer.UID,
+ isDefault: false,
+ },
+ ],
+ prefs: {
+ attachSignature: false,
+ attachVCard: false,
+ autoQuote: true,
+ catchAll: false,
+ catchAllHint: null,
+ composeHtml: true,
+ email: "username@hostname",
+ escapedVCard: null,
+ fullName: "User",
+ htmlSigFormat: false,
+ htmlSigText: "",
+ label: "",
+ organization: "",
+ replyOnTop: 0,
+ replyTo: null,
+ sigBottom: true,
+ sigOnForward: false,
+ sigOnReply: true,
+ },
+ smtpID: smtpServerA.UID,
+ });
+
+ Assert.equal(MailServices.accounts.allIdentities.length, 3);
+ Assert.equal(accountA.identities.length, 2);
+
+ let newIdentity = MailServices.accounts.allIdentities.find(
+ i => i.UID == newIdentityID
+ );
+ Assert.equal(newIdentity.email, "username@hostname");
+ Assert.equal(newIdentity.fullName, "User");
+ Assert.equal(newIdentity.smtpServerKey, smtpServerA.key);
+ Assert.equal(accountA.defaultIdentity.key, identityA.key);
+
+ await store.applyIncoming({
+ id: newIdentityID,
+ accounts: [
+ {
+ id: accountA.incomingServer.UID,
+ isDefault: true,
+ },
+ ],
+ prefs: {
+ attachSignature: false,
+ attachVCard: false,
+ autoQuote: true,
+ catchAll: false,
+ catchAllHint: null,
+ composeHtml: true,
+ email: "username@hostname",
+ escapedVCard: null,
+ fullName: "User (changed)",
+ htmlSigFormat: false,
+ htmlSigText: "",
+ label: "",
+ organization: "",
+ replyOnTop: 0,
+ replyTo: null,
+ sigBottom: true,
+ sigOnForward: false,
+ sigOnReply: true,
+ },
+ smtpID: smtpServerA.UID,
+ });
+
+ Assert.equal(newIdentity.fullName, "User (changed)");
+ Assert.equal(accountA.defaultIdentity.key, newIdentity.key);
+
+ await store.applyIncoming({
+ id: newIdentityID,
+ deleted: true,
+ });
+
+ Assert.equal(MailServices.accounts.allIdentities.length, 2);
+ Assert.equal(accountA.identities.length, 1);
+ Assert.equal(accountA.defaultIdentity.key, identityA.key);
+ Assert.equal(accountB.identities.length, 1);
+ Assert.equal(accountB.defaultIdentity.key, identityB.key);
+});
diff --git a/comm/mail/services/sync/test/unit/test_identity_tracker.js b/comm/mail/services/sync/test/unit/test_identity_tracker.js
new file mode 100644
index 0000000000..eda39a1794
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/test_identity_tracker.js
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+do_get_profile();
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { IdentitiesEngine } = ChromeUtils.importESModule(
+ "resource://services-sync/engines/identities.sys.mjs"
+);
+const { Service } = ChromeUtils.importESModule(
+ "resource://services-sync/service.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+
+let engine, store, tracker;
+let accountA, smtpServerA, smtpServerB, identityA, identityB;
+
+add_setup(async function () {
+ engine = new IdentitiesEngine(Service);
+ await engine.initialize();
+ store = engine._store;
+ tracker = engine._tracker;
+
+ Assert.equal(tracker.score, 0);
+ Assert.equal(tracker._isTracking, false);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ try {
+ // Ensure there is a local mail account...
+ MailServices.accounts.localFoldersServer;
+ } catch {
+ // ... if not, make one.
+ MailServices.accounts.createLocalMailAccount();
+ }
+
+ accountA = MailServices.accounts.createAccount();
+ accountA.incomingServer = MailServices.accounts.createIncomingServer(
+ "username",
+ "hostname",
+ "imap"
+ );
+ smtpServerA = MailServices.smtp.createServer();
+ smtpServerB = MailServices.smtp.createServer();
+
+ identityA = MailServices.accounts.createIdentity();
+ identityA.email = "identity.a@hostname";
+ identityA.fullName = "Identity A";
+ identityA.smtpServerKey = smtpServerA.key;
+ accountA.addIdentity(identityA);
+
+ identityB = MailServices.accounts.createIdentity();
+ identityB.email = "identity.b@hostname";
+ identityB.fullName = "Identity B";
+ identityB.smtpServerKey = smtpServerB.key;
+ accountA.addIdentity(identityB);
+
+ Assert.equal(MailServices.accounts.allIdentities.length, 2);
+ Assert.equal(accountA.identities.length, 2);
+ Assert.equal(accountA.defaultIdentity.key, identityA.key);
+
+ tracker.start();
+ Assert.equal(tracker._isTracking, true);
+});
+
+/**
+ * Test creating, changing, and deleting an identity that should be synced.
+ */
+add_task(async function testIdentity() {
+ Assert.equal(tracker.score, 0);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ let id = newUID();
+ let newIdentity = MailServices.accounts.createIdentity();
+ newIdentity.UID = id;
+ newIdentity.email = "username@hostname";
+ newIdentity.fullName = "User";
+ newIdentity.smtpServerKey = smtpServerA.key;
+ accountA.addIdentity(newIdentity);
+
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ tracker.resetScore();
+ Assert.equal(tracker.score, 0);
+
+ newIdentity.fullName = "Changed name";
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ newIdentity.label = "Changed label";
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ newIdentity.smtpServerKey = smtpServerB.key;
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ newIdentity.smtpServerKey = null;
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ accountA.removeIdentity(newIdentity);
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), { [id]: 0 });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+});
+
+/**
+ * Test swapping the default identity of an account.
+ */
+add_task(async function testDefaultIdentityChange() {
+ Assert.equal(tracker.score, 0);
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+
+ accountA.defaultIdentity = identityB;
+
+ Assert.equal(tracker.score, 301);
+ Assert.deepEqual(await tracker.getChangedIDs(), {
+ [identityA.UID]: 0,
+ [identityB.UID]: 0,
+ });
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+});
+
+/**
+ * Test the store methods on identites. The tracker should ignore them.
+ */
+add_task(async function testIncomingChanges() {
+ let id = newUID();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ accounts: [
+ {
+ id: accountA.UID,
+ isDefault: true,
+ },
+ ],
+ prefs: {
+ attachSignature: false,
+ attachVCard: false,
+ autoQuote: true,
+ catchAll: false,
+ catchAllHint: null,
+ composeHtml: true,
+ email: "username@hostname",
+ escapedVCard: null,
+ fullName: "User",
+ htmlSigFormat: false,
+ htmlSigText: "",
+ label: "",
+ organization: "",
+ replyOnTop: 0,
+ replyTo: null,
+ sigBottom: true,
+ sigOnForward: false,
+ sigOnReply: true,
+ },
+ smtpID: smtpServerA.UID,
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.clearChangedIDs();
+ tracker.resetScore();
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ accounts: [
+ {
+ id: accountA.UID,
+ isDefault: true,
+ },
+ ],
+ prefs: {
+ attachSignature: false,
+ attachVCard: false,
+ autoQuote: true,
+ catchAll: false,
+ catchAllHint: null,
+ composeHtml: true,
+ email: "username@hostname",
+ escapedVCard: null,
+ fullName: "User (changed)",
+ htmlSigFormat: false,
+ htmlSigText: "",
+ label: "",
+ organization: "",
+ replyOnTop: 0,
+ replyTo: null,
+ sigBottom: true,
+ sigOnForward: false,
+ sigOnReply: true,
+ },
+ smtpID: smtpServerA.UID,
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+
+ tracker.ignoreAll = true;
+ await store.applyIncoming({
+ id,
+ deleted: true,
+ });
+ tracker.ignoreAll = false;
+
+ Assert.deepEqual(await tracker.getChangedIDs(), {});
+ Assert.equal(tracker.score, 0);
+});
diff --git a/comm/mail/services/sync/test/unit/xpcshell.ini b/comm/mail/services/sync/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..a8de1816a6
--- /dev/null
+++ b/comm/mail/services/sync/test/unit/xpcshell.ini
@@ -0,0 +1,11 @@
+[default]
+head = head.js
+
+[test_account_store.js]
+[test_account_tracker.js]
+[test_addressBook_store.js]
+[test_addressBook_tracker.js]
+[test_calendar_store.js]
+[test_calendar_tracker.js]
+[test_identity_store.js]
+[test_identity_tracker.js]