From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../services/sync/modules/engines/accounts.sys.mjs | 392 ++++++++++++++++++++ .../sync/modules/engines/addressBooks.sys.mjs | 380 ++++++++++++++++++++ .../sync/modules/engines/calendars.sys.mjs | 349 ++++++++++++++++++ .../sync/modules/engines/identities.sys.mjs | 394 +++++++++++++++++++++ comm/mail/services/sync/moz.build | 14 + comm/mail/services/sync/test/unit/head.js | 7 + .../services/sync/test/unit/test_account_store.js | 361 +++++++++++++++++++ .../sync/test/unit/test_account_tracker.js | 155 ++++++++ .../sync/test/unit/test_addressBook_store.js | 130 +++++++ .../sync/test/unit/test_addressBook_tracker.js | 167 +++++++++ .../services/sync/test/unit/test_calendar_store.js | 180 ++++++++++ .../sync/test/unit/test_calendar_tracker.js | 154 ++++++++ .../services/sync/test/unit/test_identity_store.js | 222 ++++++++++++ .../sync/test/unit/test_identity_tracker.js | 238 +++++++++++++ comm/mail/services/sync/test/unit/xpcshell.ini | 11 + 15 files changed, 3154 insertions(+) create mode 100644 comm/mail/services/sync/modules/engines/accounts.sys.mjs create mode 100644 comm/mail/services/sync/modules/engines/addressBooks.sys.mjs create mode 100644 comm/mail/services/sync/modules/engines/calendars.sys.mjs create mode 100644 comm/mail/services/sync/modules/engines/identities.sys.mjs create mode 100644 comm/mail/services/sync/moz.build create mode 100644 comm/mail/services/sync/test/unit/head.js create mode 100644 comm/mail/services/sync/test/unit/test_account_store.js create mode 100644 comm/mail/services/sync/test/unit/test_account_tracker.js create mode 100644 comm/mail/services/sync/test/unit/test_addressBook_store.js create mode 100644 comm/mail/services/sync/test/unit/test_addressBook_tracker.js create mode 100644 comm/mail/services/sync/test/unit/test_calendar_store.js create mode 100644 comm/mail/services/sync/test/unit/test_calendar_tracker.js create mode 100644 comm/mail/services/sync/test/unit/test_identity_store.js create mode 100644 comm/mail/services/sync/test/unit/test_identity_tracker.js create mode 100644 comm/mail/services/sync/test/unit/xpcshell.ini (limited to 'comm/mail/services/sync') 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] -- cgit v1.2.3