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, 2011
 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  * MailList is an abstraction of a mailing list that facilitates getting the
 43  * cards contained within the actual list as well as accessing and modifying the
 44  * list and its properties.
 45  *
 46  * @param aList {Components.interfaces.nsIAbDirectory}      The actual nsIAbDirectory
 47  *                                       representation of a mailing list.
 48  * @param aParentDirectory {AddressBook} The parent directory (as an
 49  *                                       AddressBook object) containing this
 50  *                                       mailing list.
 51  * @param aNew             {boolean}     Set as true for new mailing lists where
 52  *                                       no attempt should be made to fetch the
 53  *                                       contacts contained in the list.
 54  * @constructor
 55  * @class
 56  */
 57 com.gContactSync.MailList = function gCS_MailList(aList, aParentDirectory, aNew) {
 58   if (!aParentDirectory ||
 59     !(aParentDirectory instanceof com.gContactSync.AddressBook ||
 60         aParentDirectory instanceof com.gContactSync.GAddressBook))
 61     throw "Error - invalid address book supplied to the MailList Constructor";
 62   this.mParent = aParentDirectory;
 63   this.mParent.checkList(aList, "MailList constructor");
 64   this.mList   = aList;
 65   this.mList.QueryInterface(Components.interfaces.nsIAbMDBDirectory);
 66   this.mNew    = aNew;
 67   this.mIgnoreIfBroken = false;
 68   if (!aNew)
 69     this.getAllContacts();
 70 };
 71 
 72 com.gContactSync.MailList.prototype = {
 73   /** The contacts in this mailing list (cached) */
 74   mContacts:       [],
 75   /** This is true whenever the contacts have to be fetched again */
 76   mContactsUpdate: false,
 77   /**
 78    * Sets the name of this list. The update method must be called in order for
 79    * the change to become permanent.
 80    * @param aName {string} The new name for the list.
 81    */
 82   setName: function MailList_setName(aName) {
 83     this.mList.dirName = aName;
 84   },
 85   /**
 86    * Returns the name of this list.
 87    * @returns {string} The name of this list.
 88    */
 89   getName: function MailList_getName() {
 90     return this.mList.dirName;
 91   },
 92   /**
 93    * Returns the card in this mail list, if any, with the same (not-null)
 94    * value for the GoogleID attribute, or, if the GoogleID is null, if the
 95    *         display name, primary, and second emails are the same.
 96    * @param aContact {TBContact} The contact being searched for.
 97    * @param aAttrs   {Array} The attributes whose values must be identical in
 98    *                         order for the contact to match.  The defaults are
 99    *                         DisplayName, PrimaryEmail, and SecondEmail.
100    *                         This is only used if the contact doesn't have a
101    *                         GoogleID
102    * @returns {TBContact} The card in this list, if any, with the same, and
103    *                      non-null value for its GoogleID attribute, or, if the
104    *                      GoogleID is null, if the display name, primary, and
105    *                      second emails are the same.
106    */
107   hasContact: function MailList_hasContact(aContact, aAttrs) {
108     if (!(aContact instanceof com.gContactSync.TBContact)) {
109       throw "Invalid aContact sent to MailList.hasContact";
110     }
111     // get all of the cards in this list again, if necessary
112     if (this.mContactsUpdate || this.mContacts.length === 0) {
113       this.getAllContacts();
114     }
115     // the attributes to check
116     var attrs = aAttrs ? aAttrs : ["DisplayName", "PrimaryEmail", "SecondEmail"];
117     for (var i = 0, length = this.mContacts.length; i < length; i++) {
118       var contact    = this.mContacts[i],
119           aContactID = aContact.getID();
120       // if it is an old card (has id) compare IDs
121       if (aContactID) {
122         if (aContactID === contact.getID()) {
123           return contact;
124         }
125       }
126       // else check that display name, primary and second email are equal
127       else {
128         for (var j = 0; j < attrs.length; j++) {
129           var aContactVal = aContact.getValue(attrs[j]),
130               contactVal  = contact.getValue(attrs[j]);
131           // if a value is non-empty and the two are not equal then return false
132           if ((aContactVal || contactVal) && aContactVal !== contactVal) {
133             return false;
134           }
135         }
136         return contact;
137       }
138     }
139     return null;
140   },
141   /**
142    * Sets the nick name for this mailing list.  The update method must be
143    * called in order for the change to become permanent.
144    * @param aNickName {string} The new nick name for this mailing list.
145    */
146   setNickName: function MailList_setNickName(aNickName) {
147     this.mList.listNickName = aNickName;
148   },
149   /**
150    * Returns the nick name of this mailing list.
151    * @returns {string} The nick name of this mailing list.
152    */
153   getNickName: function MailList_getNickName() {
154     return this.mList.listNickName;
155   },
156   /**
157    * Sets the description for this mailing list.  The update method must be
158    * called in order for the change to become permanent.
159    * @param aDescription {string} The new description for this mailing list.
160    */
161   setDescription: function MailList_setDescription(aDescription) {
162     this.mList.description = aDescription;
163   },
164   /**
165    * Returns the description of this mailing list.
166    * @returns {string} The description of this mailing list.
167    */
168   getDescription: function MailList_getDescription() {
169     return this.mList.description;
170   },
171   /**
172    * Adds a contact to this mailing list without checking if it already exists.
173    * NOTE: If the contact does not have a primary e-mail address then this
174    * method will add a fake one.
175    * @param aContact {TBContact} The contact to add to this mailing list.
176    * @returns {TBContact}  The contact.
177    */
178   addContact: function MailList_addContact(aContact) {
179     if (!(aContact instanceof com.gContactSync.TBContact)) {
180       throw "Invalid aContact sent to AddressBook.addContact";
181     }
182     // Add a dummy e-mail address if necessary and ignore the preference
183     // If this was not done then the mailing list would break.
184     if (!(aContact.getValue("PrimaryEmail"))) {
185       aContact.setValue("PrimaryEmail", com.gContactSync.makeDummyEmail(aContact, true));
186       aContact.update(); // TODO is this necessary
187     }
188     try {
189       var realContact = new com.gContactSync.TBContact(this.mList.addCard(aContact.mContact),
190                                                        this);
191       this.mContacts.push(realContact);
192       return realContact;
193     }
194     catch (e) {
195       com.gContactSync.LOGGER.LOG_ERROR("Unable to add card to the mail list with URI: " +
196                        this.getURI(), e);
197     }
198     return null;
199   },
200   /**
201    * Returns the uniform resource identifier (URI) for this mailing list.
202    * @returns {string} The URI of this list.
203    */
204   getURI: function MailList_getURI() {
205     if (this.mList.URI)
206       return this.mList.URI;
207     return this.mList.getDirUri();
208   },
209   /**
210    * Returns an array of all of the cards in this mailing list.
211    * @returns {array} An array containing all of the cards in this mailing list.
212    */
213   getAllContacts: function MailList_getAllContacts() {
214     // NOTE: Sometimes hasMoreElements fails if mail lists aren't working
215     this.mContacts = [];
216     var iter = this.mList.childCards,
217         data;
218     if (iter instanceof Components.interfaces.nsISimpleEnumerator) { // Thunderbird 3
219       try {
220         while (iter.hasMoreElements()) {
221           data = iter.getNext();
222           if (data instanceof Components.interfaces.nsIAbCard)
223             this.mContacts.push(new com.gContactSync.TBContact(data, this));
224         }
225       }
226       catch (e) {
227       
228         // If enumeration fails and the error shouldn't be ignored then offer
229         // to reset this AB for the user.
230         if (!this.mIgnoreIfBroken) {
231           com.gContactSync.LOGGER.LOG_ERROR("A mailing list is not working:", e);
232           if (com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("resetConfirm"))) {
233             if (this.mParent.reset()) {
234               com.gContactSync.alert(com.gContactSync.StringBundle.getStr("pleaseRestart"));
235             }
236           }
237           // Throw an error to stop the sync
238           throw com.gContactSync.StringBundle.getStr("mailListBroken");
239           
240         // If ignoring this broken mailing list (such as when enumerating
241         // through a list immediately after adding a contact to it) then quit.
242         // This is a VERBOSE_LOG instead of LOG_WARNING or ERROR to avoid
243         // unnecessary e-mail/forum posts.
244         } else {
245           com.gContactSync.LOGGER.VERBOSE_LOG("A mailing list is not working:", e);
246           return this.mContacts;
247         }
248       }
249     }
250     else if (iter instanceof Components.interfaces.nsIEnumerator) { // TB 2
251       // use nsIEnumerator...
252       try {
253         iter.first();
254         do {
255           data = iter.currentItem();
256           if (data instanceof Components.interfaces.nsIAbCard)
257             this.mContacts.push(new com.gContactSync.TBContact(data, this));
258           iter.next();
259         } while (Components.lastResult === 0);
260       }
261       catch (ex) {
262         // TODO find a way to distinguish between the usual errors and the
263         // broken list errors
264         // error is expected when finished
265         com.gContactSync.LOGGER.VERBOSE_LOG("This error is (sometimes) expected:\n" + ex);
266       }
267     }
268     else {
269       com.gContactSync.LOGGER.LOG_ERROR("Could not iterate through an address book's contacts");
270       throw com.gContactSync.StringBundle.getStr("mailListBroken");
271     }
272     return this.mContacts;
273   },
274   /**
275    * Deletes all of the cards in the array of cards from this list.
276    * @param aContacts {array} The array of TBContacts to delete from this mailing list.
277    */
278   deleteContacts: function MailList_deleteContacts(aContacts) {
279     if (!(aContacts && aContacts.length > 0))
280       return;
281     var arr,
282         i = 0;
283     if (com.gContactSync.AbManager.mVersion === 3) { // TB 3
284       arr = Components.classes["@mozilla.org/array;1"]
285                       .createInstance(Components.interfaces.nsIMutableArray);
286       for (; i < aContacts.length; i++) {
287         if (aContacts[i] instanceof com.gContactSync.TBContact) {
288           arr.appendElement(aContacts[i].mContact, false);
289         }
290         else {
291           com.gContactSync.LOGGER.LOG_WARNING("Found an invalid contact sent " +
292                                               "MailList.deleteContacts");
293         }
294       }
295     }
296     else { // TB 2
297       arr =  Components.classes["@mozilla.org/supports-array;1"]
298                        .createInstance(Components.interfaces.nsISupportsArray);
299       for (; i < aContacts.length; i++) {
300         if (aContacts[i] instanceof com.gContactSync.TBContact) {
301           arr.AppendElement(aContacts[i].mContact, false);
302         }
303         else {
304           com.gContactSync.LOGGER.LOG_WARNING("Found an invalid contact sent " +
305                                               "MailList.deleteContacts");
306         }
307       }
308     }
309     try {
310       if (arr) { // make sure arr isn't null (mailnews bug 448165)
311         this.mContactsUpdate = true; // update mContacts when used
312         this.mList.deleteCards(arr);
313       }
314     }
315     catch (e) {
316       com.gContactSync.LOGGER.LOG_WARNING("Error while deleting cards from a mailing list", e);
317     }
318     this.mContacts = this.getAllContacts();
319   },
320   /**
321    * Deletes this mailing list from its parent address book.
322    */
323   remove: function MailList_delete() {
324     this.mParent.mDirectory.deleteDirectory(this.mList);
325     this.mContacts = [];
326     // make sure the functions don't do anything
327     for (var i in this) {
328       if (i instanceof Function)
329         i = function () {};
330     }
331   },
332   /**
333    * Updates this mail list (commits changes like renaming or changing the
334    * nickname)
335    */
336   update: function MailList_update() {
337     try {
338       if (com.gContactSync.AbManager.mVersion === 3)
339         this.mList.editMailListToDatabase(null);
340       else
341         this.mList.editMailListToDatabase(this.getURI(), null);
342     }
343     catch (e) {
344       com.gContactSync.LOGGER.LOG_WARNING("Unable to update mail list", e);
345     }
346   },
347   /**
348    * Tells this mailing list whether it should avoid asking the user to confirm
349    * a reset if broken.
350    * @param aIgnore {boolean} Set this to true to avoid notifying the user of
351    *                          a problem if this list is broken.
352    */
353   setIgnoreIfBroken: function MailList_setIgnoreIfBroken(aIgnore) {
354     this.mIgnoreIfBroken = aIgnore;
355   }
356 };
357