/* 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) ); }, };