/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */var{MailServices}=ChromeUtils.importESModule("resource:///modules/MailServices.sys.mjs");var{AddrBookDirectory}=ChromeUtils.importESModule("resource:///modules/AddrBookDirectory.sys.mjs");XPCOMUtils.defineLazyGlobalGetters(this,["fetch","File","FileReader"]);ChromeUtils.defineESModuleGetters(this,{AddrBookCard:"resource:///modules/AddrBookCard.sys.mjs",BANISHED_PROPERTIES:"resource:///modules/VCardUtils.sys.mjs",VCardProperties:"resource:///modules/VCardUtils.sys.mjs",VCardPropertyEntry:"resource:///modules/VCardUtils.sys.mjs",VCardUtils:"resource:///modules/VCardUtils.sys.mjs",newUID:"resource:///modules/AddrBookUtils.sys.mjs",});// nsIAbCard.idl contains a list of properties that Thunderbird uses. Extensions are not// restricted to using only these properties, but the following properties cannot// be modified by an extension.consthiddenProperties=["DbRowID","LowercasePrimaryEmail","LastModifiedDate","PopularityIndex","RecordKey","UID","_etag","_href","_vCard","vCard","PhotoName","PhotoURL","PhotoType",];/** * Reads a DOM File and returns a Promise for its dataUrl. * * @param {File} file * @returns {string} */functiongetDataUrl(file){returnnewPromise((resolve,reject)=>{varreader=newFileReader();reader.readAsDataURL(file);reader.onload=function(){resolve(reader.result);};reader.onerror=function(error){reject(newExtensionError(error));};});}/** * Returns the image type of the given contentType string, or throws if the * contentType is not an image type supported by the address book. * * @param {string} contentType - The contentType of a photo. * @returns {string} - Either "png" or "jpeg". Throws otherwise. */functiongetImageType(contentType){consttypeParts=contentType.toLowerCase().split("/");if(typeParts[0]!="image"||!["jpeg","png"].includes(typeParts[1])){thrownewExtensionError(`Unsupported image format: ${contentType}`);}returntypeParts[1];}/** * Adds a PHOTO VCardPropertyEntry for the given photo file. * * @param {VCardProperties} vCardProperties * @param {File} photoFile * @returns {VCardPropertyEntry} */asyncfunctionaddVCardPhotoEntry(vCardProperties,photoFile){constdataUrl=awaitgetDataUrl(photoFile);if(vCardProperties.getFirstValue("version")=="4.0"){vCardProperties.addEntry(newVCardPropertyEntry("photo",{},"url",dataUrl));}else{// If vCard version is not 4.0, default to 3.0.vCardProperties.addEntry(newVCardPropertyEntry("photo",{encoding:"B",type:getImageType(photoFile.type).toUpperCase()},"binary",dataUrl.substring(dataUrl.indexOf(",")+1)));}}/** * Returns a DOM File object for the contact photo of the given contact. * * @param {string} id - The id of the contact * @returns {File} The photo of the contact, or null. */asyncfunctiongetPhotoFile(id){const{item}=addressBookCache.findContactById(id);constphotoUrl=item.photoURL;if(!photoUrl){returnnull;}try{if(photoUrl.startsWith("file://")){constrealFile=Services.io.newURI(photoUrl).QueryInterface(Ci.nsIFileURL).file;constfile=awaitFile.createFromNsIFile(realFile);consttype=getImageType(file.type);// Clone the File object to be able to give it the correct name, matching// the dataUrl/webUrl code path below.returnnewFile([file],`${id}.${type}`,{type:`image/${type}`});}// Retrieve dataUrls or webUrls.constresult=awaitfetch(photoUrl);consttype=getImageType(result.headers.get("content-type"));constblob=awaitresult.blob();returnnewFile([blob],`${id}.${type}`,{type:`image/${type}`});}catch(ex){console.error(`Failed to read photo information for ${id}: `+ex);}returnnull;}/** * Sets the provided file as the primary photo of the given contact. * * @param {string} id - The id of the contact * @param {File} file - The new photo */asyncfunctionsetPhotoFile(id,file){constnode=addressBookCache.findContactById(id);constvCardProperties=vCardPropertiesFromCard(node.item);try{consttype=getImageType(file.type);// If the contact already has a photoUrl, replace it with the same url type.// Otherwise save the photo as a local file, except for CardDAV contacts.constphotoUrl=node.item.photoURL;constparentNode=addressBookCache.findAddressBookById(node.parentId);constuseFile=photoUrl?photoUrl.startsWith("file://"):parentNode.item.dirType!=Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE;if(useFile){letoldPhotoFile;if(photoUrl){try{oldPhotoFile=Services.io.newURI(photoUrl).QueryInterface(Ci.nsIFileURL).file;}catch(ex){console.error(`Ignoring invalid photoUrl ${photoUrl}: `+ex);}}constpathPhotoFile=awaitIOUtils.createUniqueFile(PathUtils.join(PathUtils.profileDir,"Photos"),`${id}.${type}`,0o600);if(file.mozFullPath){// The file object was created by selecting a real file through a file// picker and is directly linked to a local file. Do a low level copy.awaitIOUtils.copy(file.mozFullPath,pathPhotoFile);}else{// The file object is a data blob. Dump it into a real file.constbuffer=awaitfile.arrayBuffer();awaitIOUtils.write(pathPhotoFile,newUint8Array(buffer));}// Set the PhotoName.node.item.setProperty("PhotoName",PathUtils.filename(pathPhotoFile));// Delete the old photo file.if(oldPhotoFile?.exists()){try{awaitIOUtils.remove(oldPhotoFile.path);}catch(ex){console.error(`Failed to delete old photo file for ${id}: `+ex);}}}else{// Follow the UI and replace the entire entry.vCardProperties.clearValues("photo");awaitaddVCardPhotoEntry(vCardProperties,file);}parentNode.item.modifyCard(node.item);}catch(ex){thrownewExtensionError(`Failed to read new photo information for ${id}: `+ex);}}/** * Gets the VCardProperties of the given card either directly or by reconstructing * from a set of flat standard properties. * * @param {nsIAbCard|AddrBookCard} card * @returns {VCardProperties} */functionvCardPropertiesFromCard(card){if(card.supportsVCard){returncard.vCardProperties;}returnVCardProperties.fromPropertyMap(newMap(Array.from(card.properties,p=>[p.name,p.value])));}/** * Creates a new AddrBookCard from a set of flat standard properties. * * @param {ContactProperties} properties - A key/value properties object. * @param {string} [uid] - Optional UID for the card. * @returns {AddrBookCard} */functionflatPropertiesToAbCard(properties,uid){// Do not use VCardUtils.propertyMapToVCard().constvCard=VCardProperties.fromPropertyMap(newMap(Object.entries(properties))).toVCard();returnVCardUtils.vCardToAbCard(vCard,uid);}/** * Checks if the given property is a custom contact property, which can be exposed * to WebExtensions. * * @param {string} name - Property name. * @returns {boolean} */functionisCustomProperty(name){return(!hiddenProperties.includes(name)&&!BANISHED_PROPERTIES.includes(name)&&name.match(/^\w+$/));}/** * Adds the provided originalProperties to the card, adjusted by the changes * given in updateProperties. All banished properties are skipped and the * updated properties must be valid according to isCustomProperty(). * * @param {AddrBookCard} card - A card to receive the provided properties. * @param {ContactProperties} updateProperties - A key/value object with properties. * to update the provided originalProperties * @param {nsIProperties} originalProperties - Properties to be cloned onto * the provided card. */functionaddProperties(card,updateProperties,originalProperties){constupdates=Object.entries(updateProperties).filter(e=>isCustomProperty(e[0]));constmergedProperties=originalProperties?newMap([...Array.from(originalProperties,p=>[p.name,p.value]),...updates,]):newMap(updates);for(const[name,value]ofmergedProperties){if(!BANISHED_PROPERTIES.includes(name)&&value!=""&&value!=null&&value!=undefined){card.setProperty(name,value);}}}/** * Address book that supports finding cards only for a search (like LDAP). * * @implements {nsIAbDirectory} */classExtSearchBookextendsAddrBookDirectory{constructor(extension,args={}){super();this._readOnly=true;this._isSecure=Boolean(args.isSecure);this._dirName=String(args.addressBookName??extension.name);this._fileName="";this._uid=String(args.id??newUID());this._uri="searchaddr://"+this.UID;this.lastModifiedDate=0;this.isMailList=false;this.listNickName="";this.description="";this._dirPrefId="";}/** * @see {AddrBookDirectory} */getlists(){returnnewMap();}/** * @see {AddrBookDirectory} */getcards(){returnnewMap();}// nsIAbDirectorygetisRemote(){returntrue;}getisSecure(){returnthis._isSecure;}getCardFromProperty(){returnnull;}getCardsFromProperty(){return[];}getdirType(){returnCi.nsIAbManager.ASYNC_DIRECTORY_TYPE;}getposition(){return0;}getchildCardCount(){return0;}useForAutocomplete(){// AddrBookDirectory defaults to truereturnfalse;}getsupportsMailingLists(){returnfalse;}setLocalizedStringValue(){}asyncsearch(aQuery,aSearchString,aListener){addressBookCache.emit(`provider-search-request-${this.UID}`,aQuery,aSearchString,aListener);}}/** * Cache of items in the address book "tree". * * @implements {nsIObserver} */varaddressBookCache=new(classextendsEventEmitter{constructor(){super();this.listenerCount=0;this.flush();}_makeContactNode(contact,parent){contact.QueryInterface(Ci.nsIAbCard);return{id:contact.UID,parentId:parent.UID,type:"contact",item:contact,};}_makeDirectoryNode(directory,parent=null){directory.QueryInterface(Ci.nsIAbDirectory);constnode={id:directory.UID,type:directory.isMailList?"mailingList":"addressBook",item:directory,};if(parent){node.parentId=parent.UID;}returnnode;}_populateListContacts(mailingList){mailingList.contacts=newMap();for(constcontactofmailingList.item.childCards){constnewNode=this._makeContactNode(contact,mailingList.item);mailingList.contacts.set(newNode.id,newNode);}}getListContacts(mailingList){if(!mailingList.contacts){this._populateListContacts(mailingList);}return[...mailingList.contacts.values()];}_populateContacts(addressBook){addressBook.contacts=newMap();for(constcontactofaddressBook.item.childCards){if(!contact.isMailList){constnewNode=this._makeContactNode(contact,addressBook.item);this._contacts.set(newNode.id,newNode);addressBook.contacts.set(newNode.id,newNode);}}}getContacts(addressBook){if(!addressBook.contacts){this._populateContacts(addressBook);}return[...addressBook.contacts.values()];}_populateMailingLists(parent){parent.mailingLists=newMap();for(constmailingListofparent.item.childNodes){constnewNode=this._makeDirectoryNode(mailingList,parent.item);this._mailingLists.set(newNode.id,newNode);parent.mailingLists.set(newNode.id,newNode);}}getMailingLists(parent){if(!parent.mailingLists){this._populateMailingLists(parent);}return[...parent.mailingLists.values()];}getaddressBooks(){if(!this._addressBooks){this._addressBooks=newMap();for(consttldofMailServices.ab.directories){this._addressBooks.set(tld.UID,this._makeDirectoryNode(tld));}}returnthis._addressBooks;}flush(){this._contacts=newMap();this._mailingLists=newMap();this._addressBooks=null;}findAddressBookById(id){constaddressBook=this.addressBooks.get(id);if(addressBook){returnaddressBook;}thrownewExtensionUtils.ExtensionError(`addressBook with id=${id} could not be found.`);}findMailingListById(id){if(this._mailingLists.has(id)){returnthis._mailingLists.get(id);}for(constaddressBookofthis.addressBooks.values()){if(!addressBook.mailingLists){this._populateMailingLists(addressBook);if(addressBook.mailingLists.has(id)){returnaddressBook.mailingLists.get(id);}}}thrownewExtensionUtils.ExtensionError(`mailingList with id=${id} could not be found.`);}findContactById(id,bookHint){if(this._contacts.has(id)){returnthis._contacts.get(id);}if(bookHint&&!bookHint.contacts){this._populateContacts(bookHint);if(bookHint.contacts.has(id)){returnbookHint.contacts.get(id);}}for(constaddressBookofthis.addressBooks.values()){if(!addressBook.contacts){this._populateContacts(addressBook);if(addressBook.contacts.has(id)){returnaddressBook.contacts.get(id);}}}thrownewExtensionUtils.ExtensionError(`contact with id=${id} could not be found.`);}asyncconvert(node,extension,complete){if(node===null){returnnode;}if(Array.isArray(node)){constcards=awaitPromise.allSettled(node.map(i=>this.convert(i,extension,complete)));returncards.filter(card=>card.value).map(card=>card.value);}constcopy={};for(constkeyof["id","parentId","type"]){if(keyinnode){copy[key]=node[key];}}if(complete){if(node.type=="addressBook"){copy.mailingLists=awaitthis.convert(this.getMailingLists(node),extension,true);copy.contacts=awaitthis.convert(this.getContacts(node),extension,true);}if(node.type=="mailingList"){copy.contacts=awaitthis.convert(this.getListContacts(node),extension,true);}}switch(node.type){case"addressBook":copy.name=node.item.dirName;copy.readOnly=node.item.readOnly;copy.remote=node.item.isRemote;break;case"contact":{// Clone the vCardProperties of this contact, so we can manipulate them// for the WebExtension, but do not actually change the stored data.constvCardProperties=vCardPropertiesFromCard(node.item).clone();constproperties={};// Build a flat property list from vCardProperties.for(const[name,value]ofvCardProperties.toPropertyMap()){properties[name]=""+value;}// Return all other exposed properties stored in the nodes property bag.for(constpropertyofArray.from(node.item.properties).filter(e=>isCustomProperty(e.name))){properties[property.name]=""+property.value;}// If this card has no photo vCard entry, but a local photo, add it to its vCard: Thunderbird// does not store photos of local address books in the internal _vCard property, to reduce// the amount of data stored in its database.constphotoName=node.item.getProperty("PhotoName","");constvCardPhoto=vCardProperties.getFirstValue("photo");if(!vCardPhoto&&photoName){try{constrealPhotoFile=Services.dirsvc.get("ProfD",Ci.nsIFile);realPhotoFile.append("Photos");realPhotoFile.append(photoName);constphotoFile=awaitFile.createFromNsIFile(realPhotoFile);awaitaddVCardPhotoEntry(vCardProperties,photoFile);}catch(ex){console.error(`Failed to read photo information for ${node.id}: `+ex);}}// Add the vCard.properties.vCard=vCardProperties.toVCard();if(extension.manifest.manifest_version<3){copy.properties=properties;}else{copy.vCard=properties.vCard;}letparentNode;try{parentNode=this.findAddressBookById(node.parentId);}catch(ex){// Parent might be a mailing list.parentNode=this.findMailingListById(node.parentId);}copy.readOnly=parentNode.item.readOnly;copy.remote=parentNode.item.isRemote;break;}case"mailingList":{copy.name=node.item.dirName;copy.nickName=node.item.listNickName;copy.description=node.item.description;constparentNode=this.findAddressBookById(node.parentId);copy.readOnly=parentNode.item.readOnly;copy.remote=parentNode.item.isRemote;break;}}returncopy;}// nsIObserver_notifications=["addrbook-directory-created","addrbook-directory-updated","addrbook-directory-deleted","addrbook-contact-created","addrbook-contact-properties-updated","addrbook-contact-deleted","addrbook-list-created","addrbook-list-updated","addrbook-list-deleted","addrbook-list-member-added","addrbook-list-member-removed",];observe(subject,topic,data){switch(topic){case"addrbook-directory-created":{subject.QueryInterface(Ci.nsIAbDirectory);constnewNode=this._makeDirectoryNode(subject);if(this._addressBooks){this._addressBooks.set(newNode.id,newNode);}this.emit("address-book-created",newNode);break;}case"addrbook-directory-updated":{subject.QueryInterface(Ci.nsIAbDirectory);this.emit("address-book-updated",this._makeDirectoryNode(subject));break;}case"addrbook-directory-deleted":{subject.QueryInterface(Ci.nsIAbDirectory);constuid=subject.UID;if(this._addressBooks?.has(uid)){constparentNode=this._addressBooks.get(uid);if(parentNode.contacts){for(constidofparentNode.contacts.keys()){this._contacts.delete(id);}}if(parentNode.mailingLists){for(constidofparentNode.mailingLists.keys()){this._mailingLists.delete(id);}}this._addressBooks.delete(uid);}this.emit("address-book-deleted",uid);break;}case"addrbook-contact-created":{subject.QueryInterface(Ci.nsIAbCard);constparent=MailServices.ab.getDirectoryFromUID(data);constnewNode=this._makeContactNode(subject,parent);if(this._addressBooks?.has(data)){constparentNode=this._addressBooks.get(data);if(parentNode.contacts){parentNode.contacts.set(newNode.id,newNode);}this._contacts.set(newNode.id,newNode);}this.emit("contact-created",newNode);break;}case"addrbook-contact-properties-updated":{subject.QueryInterface(Ci.nsIAbCard);constparentUID=subject.directoryUID;constparent=MailServices.ab.getDirectoryFromUID(parentUID);constnewNode=this._makeContactNode(subject,parent);if(this._addressBooks?.has(parentUID)){constparentNode=this._addressBooks.get(parentUID);if(parentNode.contacts){parentNode.contacts.set(newNode.id,newNode);this._contacts.set(newNode.id,newNode);}if(parentNode.mailingLists){for(constmailingListofparentNode.mailingLists.values()){if(mailingList.contacts&&mailingList.contacts.has(newNode.id)){mailingList.contacts.get(newNode.id).item=subject;}}}}this.emit("contact-updated",newNode,JSON.parse(data));break;}case"addrbook-contact-deleted":{subject.QueryInterface(Ci.nsIAbCard);constuid=subject.UID;this._contacts.delete(uid);if(this._addressBooks?.has(data)){constparentNode=this._addressBooks.get(data);if(parentNode.contacts){parentNode.contacts.delete(uid);}}this.emit("contact-deleted",data,uid);break;}case"addrbook-list-created":{subject.QueryInterface(Ci.nsIAbDirectory);constparent=MailServices.ab.getDirectoryFromUID(data);constnewNode=this._makeDirectoryNode(subject,parent);if(this._addressBooks?.has(data)){constparentNode=this._addressBooks.get(data);if(parentNode.mailingLists){parentNode.mailingLists.set(newNode.id,newNode);}this._mailingLists.set(newNode.id,newNode);}this.emit("mailing-list-created",newNode);break;}case"addrbook-list-updated":{subject.QueryInterface(Ci.nsIAbDirectory);constlistNode=this.findMailingListById(subject.UID);listNode.item=subject;this.emit("mailing-list-updated",listNode);break;}case"addrbook-list-deleted":{subject.QueryInterface(Ci.nsIAbDirectory);constuid=subject.UID;this._mailingLists.delete(uid);if(this._addressBooks?.has(data)){constparentNode=this._addressBooks.get(data);if(parentNode.mailingLists){parentNode.mailingLists.delete(uid);}}this.emit("mailing-list-deleted",data,uid);break;}case"addrbook-list-member-added":{subject.QueryInterface(Ci.nsIAbCard);constparentNode=this.findMailingListById(data);constnewNode=this._makeContactNode(subject,parentNode.item);if(this._mailingLists.has(data)&&this._mailingLists.get(data).contacts){this._mailingLists.get(data).contacts.set(newNode.id,newNode);}this.emit("mailing-list-member-added",newNode);break;}case"addrbook-list-member-removed":{subject.QueryInterface(Ci.nsIAbCard);constuid=subject.UID;if(this._mailingLists.has(data)){constparentNode=this._mailingLists.get(data);if(parentNode.contacts){parentNode.contacts.delete(uid);}}this.emit("mailing-list-member-removed",data,uid);break;}}}incrementListeners(){this.listenerCount++;if(this.listenerCount==1){for(consttopicofthis._notifications){Services.obs.addObserver(this,topic);}}}decrementListeners(){this.listenerCount--;if(this.listenerCount==0){for(consttopicofthis._notifications){Services.obs.removeObserver(this,topic);}this.flush();}}})();this.addressBook=classextendsExtensionAPIPersistent{persistentSearchBooks=[];hasBeenTerminated=false;PERSISTENT_EVENTS={// For primed persistent events (deactivated background), the context is only// available after fire.wakeup() has fulfilled (ensuring the convert() function// has been called).// addressBooks.*onAddressBookCreated({fire,context}){constlistener=async(event,node)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(awaitaddressBookCache.convert(node,context.extension));};addressBookCache.on("address-book-created",listener);return{unregister:()=>{addressBookCache.off("address-book-created",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onAddressBookUpdated({fire,context}){constlistener=async(event,node)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(awaitaddressBookCache.convert(node,context.extension));};addressBookCache.on("address-book-updated",listener);return{unregister:()=>{addressBookCache.off("address-book-updated",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onAddressBookDeleted({fire}){constlistener=async(event,itemUID)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(itemUID);};addressBookCache.on("address-book-deleted",listener);return{unregister:()=>{addressBookCache.off("address-book-deleted",listener);},convert(newFire){fire=newFire;},};},// contacts.*onContactCreated({fire,context}){constlistener=async(event,node)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(awaitaddressBookCache.convert(node,context.extension));};addressBookCache.on("contact-created",listener);return{unregister:()=>{addressBookCache.off("contact-created",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onContactUpdated({fire,context}){constlistener=async(event,node,changes)=>{if(fire.wakeup){awaitfire.wakeup();}constfilteredChanges={};// For MV2, report individual changed flat properties stored in the vCard// and in the property bag of the card. MV3 only sees the actual vCard.if(context.extension.manifest.manifest_version<3){if(changes.hasOwnProperty("_vCard")){constoldVCardProperties=VCardProperties.fromVCard(changes._vCard.oldValue).toPropertyMap();constnewVCardProperties=VCardProperties.fromVCard(changes._vCard.newValue).toPropertyMap();for(const[name,value]ofoldVCardProperties){if(newVCardProperties.get(name)!=value){filteredChanges[name]={oldValue:value,newValue:newVCardProperties.get(name)??null,};}}for(const[name,value]ofnewVCardProperties){if(!filteredChanges.hasOwnProperty(name)&&oldVCardProperties.get(name)!=value){filteredChanges[name]={oldValue:oldVCardProperties.get(name)??null,newValue:value,};}}}for(const[name,value]ofObject.entries(changes)){if(!filteredChanges.hasOwnProperty(name)&&isCustomProperty(name)){filteredChanges[name]=value;}}fire.sync(awaitaddressBookCache.convert(node,context.extension),filteredChanges);}elseif(changes.hasOwnProperty("_vCard")){fire.sync(awaitaddressBookCache.convert(node,context.extension),changes._vCard.oldValue);}};addressBookCache.on("contact-updated",listener);return{unregister:()=>{addressBookCache.off("contact-updated",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onContactDeleted({fire}){constlistener=async(event,parentUID,itemUID)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(parentUID,itemUID);};addressBookCache.on("contact-deleted",listener);return{unregister:()=>{addressBookCache.off("contact-deleted",listener);},convert(newFire){fire=newFire;},};},// mailingLists.*onMailingListCreated({fire,context}){constlistener=async(event,node)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(awaitaddressBookCache.convert(node,context.extension));};addressBookCache.on("mailing-list-created",listener);return{unregister:()=>{addressBookCache.off("mailing-list-created",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onMailingListUpdated({fire,context}){constlistener=async(event,node)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(awaitaddressBookCache.convert(node,context.extension));};addressBookCache.on("mailing-list-updated",listener);return{unregister:()=>{addressBookCache.off("mailing-list-updated",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onMailingListDeleted({fire}){constlistener=async(event,parentUID,itemUID)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(parentUID,itemUID);};addressBookCache.on("mailing-list-deleted",listener);return{unregister:()=>{addressBookCache.off("mailing-list-deleted",listener);},convert(newFire){fire=newFire;},};},onMemberAdded({fire,context}){constlistener=async(event,node)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(awaitaddressBookCache.convert(node,context.extension));};addressBookCache.on("mailing-list-member-added",listener);return{unregister:()=>{addressBookCache.off("mailing-list-member-added",listener);},convert(newFire,extContext){fire=newFire;context=extContext;},};},onMemberRemoved({fire}){constlistener=async(event,parentUID,itemUID)=>{if(fire.wakeup){awaitfire.wakeup();}fire.sync(parentUID,itemUID);};addressBookCache.on("mailing-list-member-removed",listener);return{unregister:()=>{addressBookCache.off("mailing-list-member-removed",listener);},convert(newFire){fire=newFire;},};},// provider.*onSearchRequest({fire},[args]){const{extension}=this;constisStarting=extension.backgroundState=="starting";constisStopped=extension.backgroundState=="stopped";letdir;// The handling of event listeners depends on the current background state// during which the listeners are registered or unregistered (Manifest V3).// starting:// Event listeners registered in this phase are in top-level background// code and will be remembered as persistent listeners. They will resume// the background script, if it has been terminated.// When the background script is re-run after being resumed, all event// listeners registered in this phase are usually skipped, except if their// parameter configuration has changed. In that case the changed listener// is re-registered with the new parameter configuration, and the old one// is unregistered during the "running" phase.//// running:// Event listeners are not registered in top-level code (but at any later// time) and will not be remembered as persistent listeners. They will// not resume the background script, and no longer work after background// termination (except another persistent listener causes the background// to resume, then it will be re-registered during re-execution of the// background script).//// suspending:// All event listeners will be unregistered when the background is being// terminated during this phase. All listeners remembered as persistent// will be immediately re-registered in the following "stopped" phase.//// stopped:// Event listeners registered during this phase are called "primed". The// background is not running, but these listeners will still be active and// resume the background script.if(isStarting&&this.hasBeenTerminated){thrownewExtensionError(`Re-registering a persistent onSearchRequest listener with different arguments, id=${args.id}.`);}constlistener=async(event,aQuery,aSearchString,aListener)=>{if(fire.wakeup){awaitfire.wakeup();}try{const{results,isCompleteResult}=awaitfire.async(awaitaddressBookCache.convert(addressBookCache.addressBooks.get(dir.UID),extension),aSearchString,aQuery);for(constresultDataofresults){letcard;// A specified vCard is winning over any individual standard property.// MV3 no longer supports flat properties.if(extension.manifest.manifest_version>2||resultData.vCard){constvCard=extension.manifest.manifest_version>2?resultData:resultData.vCard;try{card=VCardUtils.vCardToAbCard(vCard);}catch(ex){thrownewExtensionError(`Invalid vCard data: ${vCard}.`);}}else{card=flatPropertiesToAbCard(resultData);}// Add custom properties to the property bag.addProperties(card,resultData);card.directoryUID=dir.UID;aListener.onSearchFoundCard(card);}aListener.onSearchFinished(Cr.NS_OK,isCompleteResult,null,"");}catch(ex){aListener.onSearchFinished(ex.result||Cr.NS_ERROR_FAILURE,true,null,"");}};if(isStopped){// This is registering a primed listener (re-executing the exact same// register request with the same arguments), after the background script// has been terminated. Use the already existing persistent directory.dir=this.persistentSearchBooks.shift();// Remember that we have been terminated, to prevent re-registrations of// persistent listeners with changed parameter configurations.this.hasBeenTerminated=true;}else{dir=newExtSearchBook(extension,args);// Keep track of books of persistent listeners, which must not be removed// during background termination.dir.persistent=isStarting;if(addressBookCache.addressBooks.has(dir.UID)){thrownewExtensionUtils.ExtensionError(`addressBook with id=${dir.UID} already exists.`);}dir.init();MailServices.ab.addAddressBook(dir);}addressBookCache.on(`provider-search-request-${dir.UID}`,listener);return{unregister:()=>{addressBookCache.off(`provider-search-request-${dir.UID}`,listener);if(extension.backgroundState=="suspending"&&dir.persistent){// During background termination, all listeners are unregistered. All// persistent listeners will be immediately re-registered as primed// listeners and we should not remove the corresponding address books.this.persistentSearchBooks.push(dir);}else{MailServices.ab.deleteAddressBook(dir.URI);}},convert(newFire){fire=newFire;},};},};constructor(...args){super(...args);addressBookCache.incrementListeners();}onShutdown(){addressBookCache.decrementListeners();}getAPI(context){const{extension}=context;const{tabManager}=extension;constgetContactsApi=()=>({list(parentId){constparentNode=addressBookCache.findAddressBookById(parentId);returnaddressBookCache.convert(addressBookCache.getContacts(parentNode),extension,false);},asyncquery(queryInfo){const{getSearchTokens,getModelQuery,generateQueryURI}=ChromeUtils.importESModule("resource:///modules/ABQueryUtils.sys.mjs");constsearchString=queryInfo.searchString||"";constsearchWords=getSearchTokens(searchString);if(searchWords.length==0){return[];}constsearchFormat=getModelQuery("mail.addr_book.quicksearchquery.format");constsearchQuery=generateQueryURI(searchFormat,searchWords);letbooksToSearch;if(queryInfo.parentId==null){booksToSearch=[...addressBookCache.addressBooks.values()];}else{booksToSearch=[addressBookCache.findAddressBookById(queryInfo.parentId),];}constresults=[];constpromises=[];for(constbookofbooksToSearch){if((book.item.isRemote&&!queryInfo.includeRemote)||(!book.item.isRemote&&!queryInfo.includeLocal)||(book.item.readOnly&&!queryInfo.includeReadOnly)||(!book.item.readOnly&&!queryInfo.includeReadWrite)){continue;}promises.push(newPromise(resolve=>{book.item.search(searchQuery,searchString,{onSearchFinished(){resolve();},onSearchFoundCard(contact){if(contact.isMailList){return;}results.push(addressBookCache._makeContactNode(contact,book.item));},});}));}awaitPromise.all(promises);returnaddressBookCache.convert(results,extension,false);},asyncquickSearch(parentId,queryInfo){if(typeofqueryInfo=="string"){constsearchString=queryInfo;queryInfo={searchString,includeRemote:true,includeLocal:true,includeReadOnly:true,includeReadWrite:true,};}returnthis.query({...queryInfo,parentId});},get(id){returnaddressBookCache.convert(addressBookCache.findContactById(id),extension,false);},asyncgetPhoto(id){returngetPhotoFile(id);},asyncsetPhoto(id,file){returnsetPhotoFile(id,file);},create(arg1,arg2,arg3){// Manifest V2 and V3 have different parameter configuration.letparentId,id,createData;if(extension.manifest.manifest_version>2){parentId=arg1;createData=arg2;}else{parentId=arg1;id=arg2;createData=arg3;}constparentNode=addressBookCache.findAddressBookById(parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot create a contact in a read-only address book");}letcard;// A specified vCard is winning over any individual standard property.// MV3 no longer supports flat properties.if(extension.manifest.manifest_version>2||createData.vCard){constvCard=extension.manifest.manifest_version>2?createData:createData.vCard;try{card=VCardUtils.vCardToAbCard(vCard,id);}catch(ex){thrownewExtensionError(`Invalid vCard data: ${vCard}.`);}}else{card=flatPropertiesToAbCard(createData,id);}// Add custom properties to the property bag.addProperties(card,createData);// Check if the new card has an enforced UID.if(card.vCardProperties.getFirstValue("uid")){letduplicateExists=false;try{// Second argument is only a hint, all address books are checked.addressBookCache.findContactById(card.UID,parentId);duplicateExists=true;}catch(ex){// Do nothing. We want this to throw because no contact was found.}if(duplicateExists){thrownewExtensionError(`Duplicate contact id: ${card.UID}`);}}constnewCard=parentNode.item.addCard(card);returnnewCard.UID;},update(id,updateData){constnode=addressBookCache.findContactById(id);constparentNode=addressBookCache.findAddressBookById(node.parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot modify a contact in a read-only address book");}// A specified vCard is winning over any individual standard property.// While a vCard is replacing the entire contact, specified standard// properties only update single entries (setting a value to null// clears it / promotes the next value of the same kind).// MV3 no longer supports flat properties.letcard;if(extension.manifest.manifest_version>2||updateData.vCard){constvCard=extension.manifest.manifest_version>2?updateData:updateData.vCard;letvCardUID;try{card=newAddrBookCard();card.UID=node.item.UID;card.setProperty("_vCard",VCardUtils.translateVCard21(vCard));vCardUID=card.vCardProperties.getFirstValue("uid");}catch(ex){thrownewExtensionError(`Invalid vCard data: ${vCard}.`);}if(vCardUID&&vCardUID!=node.item.UID){thrownewExtensionError(`The card's UID ${node.item.UID} may not be changed: ${vCard}.`);}}else{// Get the current vCardProperties, build a propertyMap and create// vCardParsed which allows to identify all currently exposed entries// based on the typeName used in VCardUtils.sys.mjs (e.g. adr.work).constvCardProperties=vCardPropertiesFromCard(node.item);constvCardParsed=VCardUtils._parse(vCardProperties.entries);constpropertyMap=vCardProperties.toPropertyMap();// Save the old exposed state.constoldProperties=VCardProperties.fromPropertyMap(propertyMap);constoldParsed=VCardUtils._parse(oldProperties.entries);// Update the propertyMap.for(const[name,value]ofObject.entries(updateData)){propertyMap.set(name,value);}// Save the new exposed state.constnewProperties=VCardProperties.fromPropertyMap(propertyMap);constnewParsed=VCardUtils._parse(newProperties.entries);// Evaluate the differences and update the still existing entries,// mark removed items for deletion.constdeleteLog=[];for(consttypeNameofoldParsed.keys()){if(typeName=="version"){continue;}for(letidx=0;idx<oldParsed.get(typeName).length;idx++){if(newParsed.has(typeName)&&idx<newParsed.get(typeName).length){constoriginalIndex=vCardParsed.get(typeName)[idx].index;constnewEntryIndex=newParsed.get(typeName)[idx].index;vCardProperties.entries[originalIndex]=newProperties.entries[newEntryIndex];// Mark this item as handled.newParsed.get(typeName)[idx]=null;}else{deleteLog.push(vCardParsed.get(typeName)[idx].index);}}}// Remove entries which have been marked for deletion.for(constdeleteIndexofdeleteLog.sort((a,b)=>a<b)){vCardProperties.entries.splice(deleteIndex,1);}// Add new entries.for(consttypeNameofnewParsed.keys()){if(typeName=="version"){continue;}for(constnewEntryofnewParsed.get(typeName)){if(newEntry){vCardProperties.addEntry(newProperties.entries[newEntry.index]);}}}// Create a new card with the original UID from the updated vCardProperties.card=VCardUtils.vCardToAbCard(vCardProperties.toVCard(),node.item.UID);}// Clone original properties and update custom properties.addProperties(card,updateData,node.item.properties);parentNode.item.modifyCard(card);},delete(id){constnode=addressBookCache.findContactById(id);constparentNode=addressBookCache.findAddressBookById(node.parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot delete a contact in a read-only address book");}parentNode.item.deleteCards([node.item]);},// The module name is addressBook as defined in ext-mail.json.onCreated:newEventManager({context,module:"addressBook",event:"onContactCreated",extensionApi:this,}).api(),onUpdated:newEventManager({context,module:"addressBook",event:"onContactUpdated",extensionApi:this,}).api(),onDeleted:newEventManager({context,module:"addressBook",event:"onContactDeleted",extensionApi:this,}).api(),});constgetMailingListsApi=()=>({list(parentId){constparentNode=addressBookCache.findAddressBookById(parentId);returnaddressBookCache.convert(addressBookCache.getMailingLists(parentNode),extension,false);},get(id){returnaddressBookCache.convert(addressBookCache.findMailingListById(id),extension,false);},create(parentId,{name,nickName,description}){constparentNode=addressBookCache.findAddressBookById(parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot create a mailing list in a read-only address book");}constmailList=Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(Ci.nsIAbDirectory);mailList.isMailList=true;mailList.dirName=name;mailList.listNickName=nickName===null?"":nickName;mailList.description=description===null?"":description;constnewMailList=parentNode.item.addMailList(mailList);returnnewMailList.UID;},update(id,{name,nickName,description}){constnode=addressBookCache.findMailingListById(id);constparentNode=addressBookCache.findAddressBookById(node.parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot modify a mailing list in a read-only address book");}node.item.dirName=name;node.item.listNickName=nickName===null?"":nickName;node.item.description=description===null?"":description;node.item.editMailListToDatabase(null);},delete(id){constnode=addressBookCache.findMailingListById(id);constparentNode=addressBookCache.findAddressBookById(node.parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot delete a mailing list in a read-only address book");}parentNode.item.deleteDirectory(node.item);},listMembers(id){constnode=addressBookCache.findMailingListById(id);returnaddressBookCache.convert(addressBookCache.getListContacts(node),extension,false);},addMember(id,contactId){constnode=addressBookCache.findMailingListById(id);constparentNode=addressBookCache.findAddressBookById(node.parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot add to a mailing list in a read-only address book");}constcontactNode=addressBookCache.findContactById(contactId);node.item.addCard(contactNode.item);},removeMember(id,contactId){constnode=addressBookCache.findMailingListById(id);constparentNode=addressBookCache.findAddressBookById(node.parentId);if(parentNode.item.readOnly){thrownewExtensionUtils.ExtensionError("Cannot remove from a mailing list in a read-only address book");}constcontactNode=addressBookCache.findContactById(contactId);node.item.deleteCards([contactNode.item]);},// The module name is addressBook as defined in ext-mail.json.onCreated:newEventManager({context,module:"addressBook",event:"onMailingListCreated",extensionApi:this,}).api(),onUpdated:newEventManager({context,module:"addressBook",event:"onMailingListUpdated",extensionApi:this,}).api(),onDeleted:newEventManager({context,module:"addressBook",event:"onMailingListDeleted",extensionApi:this,}).api(),onMemberAdded:newEventManager({context,module:"addressBook",event:"onMemberAdded",extensionApi:this,}).api(),onMemberRemoved:newEventManager({context,module:"addressBook",event:"onMemberRemoved",extensionApi:this,}).api(),});return{addressBooks:{asyncopenUI(){constmessengerWindow=windowTracker.topNormalWindow;constabWindow=awaitmessengerWindow.toAddressBook();awaitnewPromise(resolve=>abWindow.setTimeout(resolve));constabTab=messengerWindow.document.getElementById("tabmail").tabInfo.find(t=>t.mode.name=="addressBookTab");returntabManager.convert(abTab);},asynccloseUI(){for(constwinofServices.wm.getEnumerator("mail:3pane")){consttabmail=win.document.getElementById("tabmail");for(consttaboftabmail.tabInfo.slice()){if(tab.browser?.currentURI.spec=="about:addressbook"){tabmail.closeTab(tab);}}}},list(complete=false){returnaddressBookCache.convert([...addressBookCache.addressBooks.values()],extension,complete);},get(id,complete=false){returnaddressBookCache.convert(addressBookCache.findAddressBookById(id),extension,complete);},create({name}){constdirName=MailServices.ab.newAddressBook(name,"",Ci.nsIAbManager.JS_DIRECTORY_TYPE);constdirectory=MailServices.ab.getDirectoryFromId(dirName);returndirectory.UID;},update(id,{name}){constnode=addressBookCache.findAddressBookById(id);node.item.dirName=name;},asyncdelete(id){constnode=addressBookCache.findAddressBookById(id);constdeletePromise=newPromise(resolve=>{constlistener=()=>{addressBookCache.off("address-book-deleted",listener);resolve();};addressBookCache.on("address-book-deleted",listener);});MailServices.ab.deleteAddressBook(node.item.URI);awaitdeletePromise;},// The module name is addressBook as defined in ext-mail.json.onCreated:newEventManager({context,module:"addressBook",event:"onAddressBookCreated",extensionApi:this,}).api(),onUpdated:newEventManager({context,module:"addressBook",event:"onAddressBookUpdated",extensionApi:this,}).api(),onDeleted:newEventManager({context,module:"addressBook",event:"onAddressBookDeleted",extensionApi:this,}).api(),provider:{onSearchRequest:newEventManager({context,module:"addressBook",event:"onSearchRequest",extensionApi:this,}).api(),},contacts:getContactsApi(),mailingLists:getMailingListsApi(),},contacts:getContactsApi(),mailingLists:getMailingListsApi(),};}};