1 /* ***** BEGIN LICENSE BLOCK *****
  2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3  *
  4  * The contents of this file are subject to the Mozilla Public License Version
  5  * 1.1 (the "License"); you may not use this file except in compliance with
  6  * the License. You may obtain a copy of the License at
  7  * http://www.mozilla.org/MPL/
  8  *
  9  * Software distributed under the License is distributed on an "AS IS" basis,
 10  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 11  * for the specific language governing rights and limitations under the
 12  * License.
 13  *
 14  * The Original Code is gContactSync.
 15  *
 16  * The Initial Developer of the Original Code is
 17  * Josh Geenen <gcontactsync@pirules.org>.
 18  * Portions created by the Initial Developer are Copyright (C) 2008-2009
 19  * the Initial Developer. All Rights Reserved.
 20  *
 21  * Contributor(s):
 22  *
 23  * Alternatively, the contents of this file may be used under the terms of
 24  * either the GNU General Public License Version 2 or later (the "GPL"), or
 25  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 26  * in which case the provisions of the GPL or the LGPL are applicable instead
 27  * of those above. If you wish to allow use of your version of this file only
 28  * under the terms of either the GPL or the LGPL, and not to allow others to
 29  * use your version of this file under the terms of the MPL, indicate your
 30  * decision by deleting the provisions above and replace them with the notice
 31  * and other provisions required by the GPL or the LGPL. If you do not delete
 32  * the provisions above, a recipient may use your version of this file under
 33  * the terms of any one of the MPL, the GPL or the LGPL.
 34  *
 35  * ***** END LICENSE BLOCK ***** */
 36 
 37 if (!com) var com = {}; // A generic wrapper variable
 38 // A wrapper for all GCS functions and variables
 39 if (!com.gContactSync) com.gContactSync = {};
 40 
 41 /**
 42  * A class for a Thunderbird Address Book with methods to add, modify, obtain, 
 43  * and delete cards.
 44  * @param aDirectory {nsIAbDirectory} The actual directory.
 45  * @constructor
 46  * @class
 47  */
 48 com.gContactSync.AddressBook = function gCS_AddressBook(aDirectory) {
 49   this.mDirectory = aDirectory;
 50   // make sure the directory is valid
 51   if (!this.isDirectoryValid(this.mDirectory))
 52     throw "Invalid directory supplied to the AddressBook constructor" +
 53           "\nCalled by: " + this.caller +
 54           com.gContactSync.StringBundle.getStr("pleaseReport");
 55   // get the directory's URI
 56   if (this.mDirectory.URI)
 57     this.mURI = this.mDirectory.URI;
 58   else {
 59     this.mDirectory.QueryInterface(Components.interfaces.nsIAbMDBDirectory);
 60     this.mURI = this.mDirectory.getDirUri();
 61   }
 62 };
 63 
 64 com.gContactSync.AddressBook.prototype = {
 65   /** The Uniform Resource Identifier (URI) of the directory */
 66   mURI:         {},
 67   /** The cards within this address book */
 68   mContacts:       [],
 69   /** set to true when mContacts should be updated */
 70   mContactsUpdate: false,
 71   /**
 72    * Adds the contact to this address book and returns the added contact.
 73    * @param aContact {TBContact} The contact to add.
 74    * @returns {TBContact} The newly-added contact.
 75    */
 76   addContact: function AddressBook_addContact(aContact) {
 77     if (!(aContact instanceof com.gContactSync.TBContact)) {
 78       throw "Invalid aContact sent to AddressBook.addContact";
 79     }
 80     try {
 81       var newContact = new com.gContactSync.TBContact(this.mDirectory.addCard(aContact.mContact),
 82                                                       this);
 83       this.mContacts.push(newContact);
 84       return newContact;
 85     }
 86     catch (e) {
 87       com.gContactSync.LOGGER.LOG_ERROR("Unable to add card to the directory with URI: " +
 88                        this.mURI, e);
 89     }
 90     return null;
 91   },
 92   /**
 93    * Returns an array of all of the cards in this Address Book.
 94    * @returns {array} An array of the TBContacts in this Address Book.
 95    */
 96   getAllContacts: function AddressBook_getAllContacts() {
 97     this.mContacts = [];
 98     var iter = this.mDirectory.childCards,
 99         data;
100     if (iter instanceof Components.interfaces.nsISimpleEnumerator) { // Thunderbird 3
101       while (iter.hasMoreElements()) {
102         data = iter.getNext();
103         if (data instanceof Components.interfaces.nsIAbCard && !data.isMailList)
104           this.mContacts.push(new com.gContactSync.TBContact(data, this));
105       }
106     }
107     else if (iter instanceof Components.interfaces.nsIEnumerator) { // TB 2
108       // use nsIEnumerator...
109       try {
110         iter.first();
111         do {
112           data = iter.currentItem();
113           if (data instanceof Components.interfaces.nsIAbCard &&
114               !data.isMailList)
115             this.mContacts.push(new com.gContactSync.TBContact(data, this));
116           iter.next();
117         } while (Components.lastResult === 0);
118       // An error is expected when finished
119       }
120       catch (e) {
121         com.gContactSync.LOGGER.VERBOSE_LOG("(This error is expected): " + e);
122       }
123     }
124     else {
125       com.gContactSync.LOGGER.LOG_ERROR("Could not iterate through an address book's contacts");
126       throw "Couldn't find an address book's contacts";
127     }
128     return this.mContacts;
129   },
130   /**
131    * Returns an an object containing MailList objects whose attribute name is
132    * the name of the mail list.
133    * @param skipGetCards {boolean} True to skip getting the cards of each list.
134    * @returns An object containing MailList objects.
135    */
136   getAllLists: function AddressBook_getAllLists(skipGetCards) {
137     // same in Thunderbird 2 and 3
138     com.gContactSync.LOGGER.VERBOSE_LOG("Searching for mailing lists:");
139     var iter = this.mDirectory.childNodes,
140         obj = {},
141         list,
142         id,
143         data;
144     while (iter.hasMoreElements()) {
145       data = iter.getNext();
146       if (data instanceof Components.interfaces.nsIAbDirectory && data.isMailList) {
147         list    = this.newListObj(data, this, skipGetCards);
148         obj.push(list);
149         com.gContactSync.LOGGER.VERBOSE_LOG(" * " + list.getName() + " - " + id);
150       }
151     }
152     return obj;
153   },
154   /**
155    * Finds and returns the first Mail List that matches the given nickname in
156    * this address book.
157    * @param aNickName {string} The nickname to search for.  If null then this
158    *                           function returns nothing.
159    * @returns {MailList} A new MailList object containing a list that matches the
160    *                    nickname or nothing if the list wasn't found.
161    */
162   getListByNickName: function AddressBook_getListByNickName(aNickName) {
163     if (!aNickName)
164       return null;
165     // same in Thunderbird 2 and 3
166     var iter = this.mDirectory.childNodes,
167         data;
168     while (iter.hasMoreElements()) {
169       data = iter.getNext();
170       if (data instanceof Components.interfaces.nsIAbDirectory && data.isMailList &&
171           data.listNickName === aNickName) {
172         return this.newListObj(data, this, true);
173       }
174     }
175     return null;
176   },
177   /**
178    * Creates a new mail list, adds it to the address book, and returns a
179    * MailList object containing the list.
180    * @param aName     {string} The new name for the mail list.
181    * @param aNickName {string} The nickname for the mail list.
182    * @returns {MailList} A new MailList object containing the newly-made List
183    *                    with the given name and nickname.
184    */
185   addList: function AddressBook_addList(aName, aNickName) {
186     if (!aName)
187       throw "Error - aName sent to addList is invalid";
188     if (!aNickName)
189       throw "Error - aNickName sent to addList is invalid";
190     var list          = Components.classes["@mozilla.org/addressbook/directoryproperty;1"]
191                                   .createInstance(Components.interfaces.nsIAbDirectory),
192         realList;
193     list.isMailList   = true;
194     list.dirName      = aName;
195     list.listNickName = aNickName;
196     this.mDirectory.addMailList(list);
197     // list can't be QI'd to an MDBDirectory, so the new list has to be found...
198     realList  = this.getListByNickName(aNickName);
199     return realList;
200   },
201   /**
202    * Deletes the nsIAbCards from the nsIAbDirectory Address Book.  If the cards
203    * aren't in the book nothing will happen.
204    * @param aContacts {array} The cards to delete from the directory
205    */
206   deleteContacts: function AddressBook_deleteContacts(aContacts) {
207     if (!(aContacts && aContacts.length > 0))
208       return;
209     var arr,
210         i = 0;
211     if (com.gContactSync.AbManager.mVersion === 3) { // TB 3
212       arr = Components.classes["@mozilla.org/array;1"]
213                       .createInstance(Components.interfaces.nsIMutableArray);
214       for (; i < aContacts.length; i++) {
215         if (aContacts[i] instanceof com.gContactSync.TBContact) {
216           arr.appendElement(aContacts[i].mContact, false);
217         }
218         else {
219           com.gContactSync.LOGGER.LOG_WARNING("Found an invalid contact sent " +
220                                               "AddressBook.deleteContacts");
221         }
222       }
223     }
224     else { // TB 2
225       arr =  Components.classes["@mozilla.org/supports-array;1"]
226                        .createInstance(Components.interfaces.nsISupportsArray);
227       for (; i < aContacts.length; i++) {
228         if (aContacts[i] instanceof com.gContactSync.TBContact) {
229           arr.AppendElement(aContacts[i].mContact, false);
230         }
231         else {
232           com.gContactSync.LOGGER.LOG_WARNING("Found an invalid contact sent " +
233                                               "AddressBook.deleteContacts");
234         }
235       }
236     }
237     try {
238       if (arr) { // make sure arr isn't null (mailnews bug 448165)
239         this.mContactsUpdate = true; // update mContacts when used
240         this.mDirectory.deleteCards(arr);
241       }
242     }
243     catch (e) {
244       com.gContactSync.LOGGER.LOG_WARNING("Error while deleting cards from an AB", e);
245     }
246   },
247   /**
248    * Updates a card (commits changes) in this address book.
249    * @param aContact {TBContact} The card to update.
250    */
251   updateContact: function AddressBook_updateContact(aContact) {
252     if (!(aContact instanceof com.gContactSync.TBContact)) {
253       throw "Invalid aContact sent to AddressBook.updateContact";
254     }
255     this.mContactsUpdate = true;
256     if (this.mDirectory && this.mDirectory.modifyCard)
257       this.mDirectory.modifyCard(aContact.mContact);
258     else if (aContact.mContact.editCardToDatabase)
259       aContact.mContact.editCardToDatabase(this.mURI);
260   },
261   /**
262    * Checks the validity of a mailing list and throws an error if it is invalid.
263    * @param aList        {nsIAbDirectory} An object that should be a mailing list.
264    * @param aMethodName  {string} The name of the method calling checkList (used
265    *                              when throwing the error)
266    */
267   checkList: function AddressBook_checkList(aList, aMethodName) {
268     // if it is a MailList object, get it's actual list
269     var list = aList && aList.mList ? aList.mList : aList;
270     if (!list || !(list instanceof Components.interfaces.nsIAbDirectory) || !list.isMailList) {
271       throw "Invalid list: " + aList + " sent to the '" + aMethodName +
272             "' method" +  com.gContactSync.StringBundle.getStr("pleaseReport");
273     }
274   },
275   /**
276    * Checks the validity of a directory and throws an error if it is invalid.
277    * @param aDirectory  {nsIAbDirectory} The directory to check.
278    * @param aMethodName {strong} The name of the method calling checkDirectory
279    *                             (used when throwing the error)
280    */
281   checkDirectory: function AddressBook_checkDirectory(aDirectory, aMethodName) {
282     if (!this.isDirectoryValid(aDirectory))
283       throw "Invalid Directory: " + aDirectory + " sent to the '" +
284             aMethodName + "' method" +
285             com.gContactSync.StringBundle.getStr("pleaseReport");
286   },
287   /**
288    * Checks the validity of a directory and returns false if it is invalid.
289    * @param aDirectory {nsIAbDirectory} The directory to check.
290    */
291   isDirectoryValid: function AddressBook_isDirectoryValid(aDirectory) {
292     return aDirectory && aDirectory instanceof Components.interfaces.nsIAbDirectory &&
293            aDirectory.dirName !== "" &&
294           (com.gContactSync.AbManager.mVersion === 3 || 
295            aDirectory instanceof Components.interfaces.nsIAbMDBDirectory);
296   },
297   /**
298    * Creates and returns a new TBContact in this address book.
299    * NOTE: The contact is already added to this address book.
300    * @returns {TBContact} A new TBContact in this address book.
301    */
302   newContact: function AddressBook_newContact() {
303     return this.addContact(new com.gContactSync
304                                   .TBContact(Components.classes["@mozilla.org/addressbook/cardproperty;1"]
305                                                        .createInstance(Components.interfaces.nsIAbCard),
306                                              this));
307   },
308   /**
309    * Returns true if the directory passed in is the same as the directory
310    * stored by this AddressBook object.  Two directories are considered the same
311    * if and only if their Uniform Resource Identifiers (URIs) are the same.
312    * @param aOtherDir The directory to compare with this object's directory.
313    * @returns {boolean} True if the URI of the passed directory is the same as
314    *                   the URI of the directory stored by this object.
315    */
316   equals: function AddressBook_equals(aOtherDir) {
317     // return false if the directory isn't valid
318     if (!this.isDirectoryValid(aOtherDir))
319       return false;
320     // compare the URIs
321     if (this.mDirectory.URI)
322       return this.mDirectory.URI === aOtherDir.URI;
323     return this.mDirectory.getDirUri() === aOtherDir.getDirUri();
324   },
325   /**
326    * Returns the card in this directory, if any, with the same (not-null)
327    * value for the GoogleID attribute, or, if the GoogleID is null, if the
328    *         display name, primary, and second emails are the same.
329    * @param aContact {TBContact} The card being searched for.
330    * @returns {TBContact} The card in this AB, if any, with the same, and
331    *                     non-null value for its GoogleID attribute, or, if the
332    *                     GoogleID is null, if the display name, primary, and
333    *                     second emails are the same.
334    */
335   hasContact: function AddressBook_hasContact(aContact) {
336     if (!(aContact instanceof com.gContactSync.TBContact)) {
337       throw "Invalid aContact sent to AddressBook.hasContact";
338     }
339     // get all of the cards in this list again, if necessary
340     if (this.mContactsUpdate || this.mContacts.length === 0) {
341       this.getAllContacts();
342     }
343     for (var i = 0, length = this.mContacts.length; i < length; i++) {
344       var contact    = this.mContacts[i],
345           aContactID = aContact.getID();
346       // if it is an old card (has id) compare IDs
347       if (aContactID) {
348         if (aContactID === contact.getID()) {
349           return contact;
350         }
351       }
352       // else check that display name, primary and second email are equal
353       else if (aContact.getValue("DisplayName")  === contact.getValue("DisplayName") &&
354                aContact.getValue("PrimaryEmail") === contact.getValue("PrimaryEmail") &&
355                aContact.getValue("SecondEmail")  === contact.getValue("SecondEmail")) {
356         return contact;
357       }
358     }
359     return null;
360   },
361   /**
362    * Sets the preference id for this mailing list.  The update method must be
363    * called in order for the change to become permanent.
364    * @param aPrefId {string} The new preference ID for this mailing list.
365    */
366   setPrefId: function AddressBook_setPrefId(aPrefId) {
367     this.mDirectory.dirPrefId = aPrefId;
368   },
369   /**
370    * Returns the preference ID of this directory prefixed with
371    * "extensions.gContactSync."
372    * @returns {string} The preference ID of this directory.
373    */
374   getPrefId: function AddressBook_getPrefId() {
375     return "extensions.gContactSync." + this.mDirectory.dirPrefId + ".";
376   },
377   /**
378    * Gets and returns the string preference, if possible, with the given name.
379    * Returns null if this list doesn't have a preference ID or if there was an
380    * error getting the preference.
381    * @param aName         {string} The name of the preference to get.
382    * @param aDefaultValue {string} The value to set the preference at if it
383    *                               fails.  Only used in Thunderbird 3.
384    * @returns {string} The value of the preference with the given name in the
385    *                  preference branch specified by the preference ID, if
386    *                  possible.  Otherwise null.
387    */
388   getStringPref: function AddressBook_getStringPref(aName, aDefaultValue) {
389     var id = this.getPrefId();
390     if (!id)
391       return null;
392     try {
393       var branch = Components.classes["@mozilla.org/preferences-service;1"]
394                              .getService(Components.interfaces.nsIPrefService)
395                              .getBranch(id)
396                              .QueryInterface(Components.interfaces.nsIPrefBranch2);
397       var value = branch.getCharPref(aName);
398       //com.gContactSync.LOGGER.VERBOSE_LOG("-Found the value: " + value);
399       return value;
400     }
401     // keep going if the preference doesn't exist for backward-compatibility
402     catch (e) {}
403     // now if a value was not found, use the old branch ID
404     // this is for backwards compatibility with 0.3.0a1pre2/0.2.11 and below,
405     try {
406       id = this.mDirectory.dirPrefId;
407       branch = Components.classes["@mozilla.org/preferences-service;1"]
408                          .getService(Components.interfaces.nsIPrefService)
409                          .getBranch(id)
410                          .QueryInterface(Components.interfaces.nsIPrefBranch2);
411       value = branch.getCharPref(aName);
412       // if the value exists (if it gets here, a value exists):
413       //  1) Create the pref using the new branch/method
414       //  2) Delete the old pref
415       this.setStringPref(aName, value);
416       branch.clearUserPref(aName);
417       com.gContactSync.LOGGER.VERBOSE_LOG("Found and removed an obsolete pref: " +
418                                           aName + " - " + value);
419       return value;
420     }
421     // an error is expected if the value isn't present
422     catch (e) {
423       return 0;
424     }
425     return null;
426   },
427   /**
428    * Sets the string preference, if possible, with the given name and value.
429    * @param aName  {string} The name of the preference to set.
430    * @param aValue {string} The value to which the preference is set.
431    */
432   setStringPref: function AddressBook_setStringPref(aName, aValue) {
433     var id = this.getPrefId();
434     com.gContactSync.LOGGER.VERBOSE_LOG("Setting pref named: " + aName + " to value: " + aValue +
435                        " to the branch: " + id);
436     if (!id) {
437       com.gContactSync.LOGGER.VERBOSE_LOG("Invalid ID");
438       return;
439     }
440     if (!aName) {
441       com.gContactSync.LOGGER.VERBOSE_LOG("Invalid name");
442       return;
443     }
444     try {
445       var branch = Components.classes["@mozilla.org/preferences-service;1"]
446                              .getService(Components.interfaces.nsIPrefService)
447                              .getBranch(id)
448                              .QueryInterface(Components.interfaces.nsIPrefBranch2);
449       branch.setCharPref(aName, aValue);
450     } catch (e) { com.gContactSync.LOGGER.LOG_WARNING("Error while setting directory pref", e); }
451   },
452 
453   /**
454    * Returns the name of this address book.
455    * @returns {string} The name of this address book.
456    */
457   getName: function AddressBook_getName() {
458     return this.mDirectory.dirName;
459   },
460   /**
461    * Sets the name of this address book.  Throws an error if the name is set to
462    * either the PAB or CAB's name.
463    * @param aName {string} The new name for this directory.
464    */
465   setName: function AddressBook_setName(aName) {
466     // make sure it isn't being set to the PAB or CAB name and make sure that
467     // this isn't the PAB or CAB
468     var pab = com.gContactSync.AbManager.getAbByURI("moz-abmdbdirectory://abook.mab");
469     var cab = com.gContactSync.AbManager.getAbByURI("moz-abmdbdirectory://history.mab");
470     if (aName === pab.dirName || aName === cab.dirName)
471       throw "Error - cannot rename a directory to the PAB or CAB's name";
472     if (this.getName() === pab.dirName || this.getName() === cab.dirName)
473       throw "Error - cannot rename the PAB or CAB";
474     // in TB 3, it is as simple as changing a property of the directory
475     if (com.gContactSync.AbManager.mVersion === 3)
476       this.mDirectory.dirName = aName;
477     // in TB 2 a few extra steps are necessary...
478     else {
479       /* NOTE: this code is originally from
480       * mailnews/addrbook/resources/content/addressbook.js:
481       * http://mxr.mozilla.org/mozilla1.8/source/mailnews/addrbook/resources/content/addressbook.js#353
482       */
483       var addressbook = Components.classes["@mozilla.org/addressbook;1"]
484                                   .createInstance(Components.interfaces.nsIAddressBook);
485       // the rdf service
486       var RDF = Components.classes["@mozilla.org/rdf/rdf-service;1"]
487                           .getService(Components.interfaces.nsIRDFService);
488       // get the datasource for the addressdirectory
489       var datasource = RDF.GetDataSource("rdf:addressdirectory");
490 
491       // moz-abdirectory:// is the RDF root to get all types of addressbooks.
492       var parent = RDF.GetResource("moz-abdirectory://")
493                       .QueryInterface(Components.interfaces.nsIAbDirectory);
494       // Copy existing dir type category id and mod time so they won't get reset.
495       var properties = this.mDirectory.directoryProperties;
496       properties.description = aName;
497       // Now do the modification.
498       addressbook.modifyAddressBook(datasource, parent, this.mDirectory, properties);
499     }
500   },
501   /**
502    * Returns the directory type of this address book.
503    * See mailnews/addrbook/src/nsDirPrefs.h
504    * @returns {integer} The directory type of this address book.
505    *                    2 means a normal Mork AB
506    *                    -1 means the dir type could not be found.
507    */
508   getDirType: function AddressBook_getDirType() {
509     if ("dirType" in this.mDirectory) {
510       return this.mDirectory.dirType;
511     }
512     else if ("directoryProperties" in this.mDirectory) {
513       return this.mDirectory.directoryProperties.dirType;
514     }
515     LOGGER.LOG_WARNING("Unable to find a dirType for the AB '" +
516                        this.getName() + "'");
517     return -1;
518   },
519   /**
520    * Creates a new mailing list in this directory and returns a MailList object
521    * representing the new list.
522    * @returns {MailList} A new MailList object.
523    */
524   newListObj: function AddressBook_newListObj(aList, aParentDirectory, aNew) {
525     return new com.gContactSync.MailList(aList, aParentDirectory, aNew);
526   },
527   /**
528    * Permanently deletes this address book without a confirmation dialog.
529    * This will not allow deleting the PAB or CAB and will show a popup
530    * if there is an attempt to delete one of those ABs.
531    * @returns {boolean} True if the AB was deleted.
532    */
533   deleteAB: function AddressBook_delete() {
534     return com.gContactSync.AbManager.deleteAB(this.mURI);
535   }
536 };
537