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-2010
 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 window.addEventListener("load",
 42   /** Initializes the ContactConverter class when the window has finished loading */
 43   function gCS_ContactConverterLoadListener(e) {
 44     com.gContactSync.ContactConverter.init();
 45   },
 46 false);
 47 
 48 
 49 /**
 50  * Converts contacts between Thunderbird's format (a 'card') and the Atom/XML
 51  * representation of a contact.  Must be initialized before the first use by
 52  * calling the init() function.
 53  * NOTE: The first 6 screennames of a contact from Google are stored as:
 54  * _AimScreenName, TalkScreenName, ICQScreenName, YahooScreenName, MSNScreenName
 55  * and JabberScreenName for compatibility with gContactSync 0.1b1 and the
 56  * default type for those textboxes.
 57  * @class
 58  */
 59 com.gContactSync.ContactConverter = {
 60   /** The GD Namespace */
 61   GD:            {},
 62   /** The ATOM/XML Namespace */
 63   ATOM:          {},
 64   /** The current TBContact being converted into a GContact */
 65   mCurrentCard:  {},
 66   /** An array of ContactConverter objects */
 67   mConverterArr: [],
 68   /**
 69    * Extra attributes added by this extension.  Doesn't include GoogleID or any
 70    * of the URLs.  Should be obtained w/ ContactConverter.getExtraSyncAttributes
 71    */
 72   mAddedAttributes: [
 73     "HomeFaxNumber", "OtherNumber", "ThirdEmail", "FourthEmail",
 74     "TalkScreenName", "ICQScreenName", "YahooScreenName", "MSNScreenName", 
 75     "JabberScreenName",  "PrimaryEmailType", "SecondEmailType",
 76     "ThirdEmailType", "FourthEmailType", "_AimScreenNameType",
 77     "TalkScreenNameType", "ICQScreenNameType", "YahooScreenNameType",
 78     "MSNScreenNameType", "JabberScreenNameType", "HomePhoneType",
 79     "WorkPhoneType", "FaxNumberType", "CellularNumberType", "PagerNumberType",
 80     "HomeFaxNumberType", "OtherNumberType", "Relation0", "Relation0Type",
 81     "Relation1", "Relation1Type", "Relation2", "Relation2Type", "Relation3",
 82     "Relation3Type", "CompanySymbol", "JobDescription",
 83     "WebPage1Type", "WebPage2Type"
 84   ],
 85   /** Stores whether this object has been initialized yet */
 86   mInitialized: false,
 87   /**
 88    * Initializes this object by populating the array of ConverterElement
 89    * objects and the two namespaces most commonly used by this object.
 90    */
 91   init: function ContactConverter_init() {
 92     this.GD = com.gContactSync.gdata.namespaces.GD;
 93     this.ATOM = com.gContactSync.gdata.namespaces.ATOM;
 94     var phoneTypes = com.gContactSync.Preferences.mSyncPrefs.phoneTypes.value;
 95     // ConverterElement(aElement, aTbName, aIndex, aType)
 96     // This array stores info on what tags in Google's feed sync with which
 97     // properties in Thunderbird.  gdata.contacts has info on these tags
 98     this.mConverterArr = [
 99       // Various components of a name
100       new com.gContactSync.ConverterElement("fullName",       "DisplayName",    0),
101       new com.gContactSync.ConverterElement("givenName",      "FirstName",      0),
102       new com.gContactSync.ConverterElement("familyName",     "LastName",       0),
103       new com.gContactSync.ConverterElement("additionalName", "AdditionalName", 0),
104       new com.gContactSync.ConverterElement("namePrefix",     "namePrefix",     0),
105       new com.gContactSync.ConverterElement("nameSuffix",     "nameSuffix",     0),
106       new com.gContactSync.ConverterElement("nickname",       "NickName",       0),
107       // general
108       new com.gContactSync.ConverterElement("notes",          "Notes",          0),
109       new com.gContactSync.ConverterElement("id",             "GoogleID",       0),
110       // e-mail addresses
111       new com.gContactSync.ConverterElement("email", "PrimaryEmail", 0, "other"),
112       new com.gContactSync.ConverterElement("email", "SecondEmail",  1, "other"),
113       new com.gContactSync.ConverterElement("email", "ThirdEmail",   2, "other"),
114       new com.gContactSync.ConverterElement("email", "FourthEmail",  3, "other"),
115       // IM screennames
116       new com.gContactSync.ConverterElement("im", "_AimScreenName",   0, "AIM"),
117       new com.gContactSync.ConverterElement("im", "TalkScreenName",   1, "GOOGLE_TALK"),
118       new com.gContactSync.ConverterElement("im", "ICQScreenName",    2, "ICQ"),
119       new com.gContactSync.ConverterElement("im", "YahooScreenName",  3, "YAHOO"),
120       new com.gContactSync.ConverterElement("im", "MSNScreenName",    4, "MSN"),
121       new com.gContactSync.ConverterElement("im", "JabberScreenName", 5, "JABBER"),
122       // the phone numbers
123       new com.gContactSync.ConverterElement("phoneNumber", "WorkPhone",      0, "work"),
124       new com.gContactSync.ConverterElement("phoneNumber", "HomePhone",      (phoneTypes ? 1 : 0), "home"),
125       new com.gContactSync.ConverterElement("phoneNumber", "FaxNumber",      (phoneTypes ? 2 : 0), "work_fax"),
126       new com.gContactSync.ConverterElement("phoneNumber", "CellularNumber", (phoneTypes ? 3 : 0), "mobile"),
127       new com.gContactSync.ConverterElement("phoneNumber", "PagerNumber",    (phoneTypes ? 4 : 0), "pager"),
128       new com.gContactSync.ConverterElement("phoneNumber", "HomeFaxNumber",  (phoneTypes ? 5 : 0), "home_fax"),
129       new com.gContactSync.ConverterElement("phoneNumber", "OtherNumber",    (phoneTypes ? 6 : 0), "other"),
130       // company info
131       new com.gContactSync.ConverterElement("orgTitle",          "JobTitle",       0),
132       new com.gContactSync.ConverterElement("orgName",           "Company",        0),
133       new com.gContactSync.ConverterElement("orgDepartment",     "Department",     0),
134       new com.gContactSync.ConverterElement("orgJobDescription", "JobDescription", 0),
135       new com.gContactSync.ConverterElement("orgSymbol",         "CompanySymbol",  0),
136       // the URLs from Google - Photo, Self, and Edit
137       new com.gContactSync.ConverterElement("PhotoURL", "PhotoURL", 0),
138       new com.gContactSync.ConverterElement("SelfURL",  "SelfURL",  0),
139       new com.gContactSync.ConverterElement("EditURL",  "EditURL",  0),
140       // Relation fields
141       new com.gContactSync.ConverterElement("relation", "Relation0", 0, ""),
142       new com.gContactSync.ConverterElement("relation", "Relation1", 1, ""),
143       new com.gContactSync.ConverterElement("relation", "Relation2", 2, ""),
144       new com.gContactSync.ConverterElement("relation", "Relation3", 3, ""),
145       // Websites
146       new com.gContactSync.ConverterElement("website",   "WebPage1", 0, "work"),
147       new com.gContactSync.ConverterElement("website",   "WebPage2", 1, "home"),
148     ];
149 
150     // Only synchronize (if possible) postal addresses if the preference was
151     // changed to true
152     if (com.gContactSync.Preferences.mSyncPrefs.syncAddresses.value) {
153       // Home address
154       this.mConverterArr.push(new com.gContactSync.ConverterElement("street",   "HomeAddress", 0, "home"));
155       this.mConverterArr.push(new com.gContactSync.ConverterElement("city",     "HomeCity",    0, "home"));
156       this.mConverterArr.push(new com.gContactSync.ConverterElement("region",   "HomeState",   0, "home"));
157       this.mConverterArr.push(new com.gContactSync.ConverterElement("postcode", "HomeZipCode", 0, "home"));
158       this.mConverterArr.push(new com.gContactSync.ConverterElement("country",  "HomeCountry", 0, "home"));
159       // Work address
160       this.mConverterArr.push(new com.gContactSync.ConverterElement("street",   "WorkAddress", 0, "work"));
161       this.mConverterArr.push(new com.gContactSync.ConverterElement("city",     "WorkCity",    0, "work"));
162       this.mConverterArr.push(new com.gContactSync.ConverterElement("region",   "WorkState",   0, "work"));
163       this.mConverterArr.push(new com.gContactSync.ConverterElement("postcode", "WorkZipCode", 0, "work"));
164       this.mConverterArr.push(new com.gContactSync.ConverterElement("country",  "WorkCountry", 0, "work"));
165       // full (formatted) addresses at the bottom so they have priority
166       this.mConverterArr.push(new com.gContactSync.ConverterElement("formattedAddress", "FullHomeAddress", 0, "home"));
167       this.mConverterArr.push(new com.gContactSync.ConverterElement("formattedAddress", "FullWorkAddress", 0, "work"));
168       this.mConverterArr.push(new com.gContactSync.ConverterElement("formattedAddress", "OtherAddress",    0, "other"));
169     }
170     this.mInitialized = true;
171   },
172   /**
173    * Returns an array of all of the extra attributes synced by this extension.
174    * @param aIncludeURLs {boolean} Should be true if the URL-related attributes
175    *                               should be returned.
176    */
177   getExtraSyncAttributes: function ContactConverter_getExtraSyncAttributes(aIncludeURLs, aIncludeAddresses) {
178     if (!this.mInitialized)
179       this.init();
180     var arr = this.mAddedAttributes.slice();
181     if (aIncludeURLs)
182       arr = arr.concat("PhotoURL", "SelfURL", "EditURL", "GoogleID");
183     if (aIncludeAddresses)
184       arr = arr.concat("FullHomeAddress", "FullWorkAddress", "OtherAddress");
185     return arr;
186   },
187   /**
188    * Updates or creates a GContact object's Atom/XML representation using its 
189    * complementary Address Book card.
190    * @param aTBContact    {TBContact} The address book card used to update the Atom
191    *                             feed.  Must be in an address book.
192    * @param aGContact {GContact} Optional. The GContact object with the Atom/XML
193    *                            representation of the contact, if it exists.  If
194    *                            not supplied, a contact and feed will be created.
195    * @returns {GContact} A GContact object with the Atom feed for the contact.
196    */
197   cardToAtomXML: function ContactConverter_cardToAtomXML(aTBContact, aGContact) {
198     var isNew = !aGContact,
199         ab    = aTBContact.mAddressBook,
200         arr   = this.mConverterArr,
201         i     = 0,
202         obj,
203         value,
204         type;
205     if (!aGContact)
206       aGContact = new com.gContactSync.GContact();
207     if (!this.mInitialized)
208       this.init();
209     if (!(aTBContact instanceof com.gContactSync.TBContact)) {
210       throw "Invalid TBContact sent to ContactConverter.cardToAtomXML from " +
211             this.caller;
212     }
213     if (!(ab instanceof com.gContactSync.AddressBook)) {
214       throw "Invalid TBContact (no mAddressBook) sent to " +
215             "ContactConverter.cardToAtomXML from " + this.caller;
216     }
217     this.mCurrentCard = aTBContact;
218     // set the regular properties from the array mConverterArr
219     for (i = 0, length = arr.length; i < length; i++) {
220       // skip the URLs
221       if (arr[i].tbName.indexOf("URL") !== -1 || arr[i].tbName === "GoogleID")
222         continue;
223       obj = arr[i];
224       com.gContactSync.LOGGER.VERBOSE_LOG(" * " + obj.tbName);
225       value = this.checkValue(aTBContact.getValue(obj.tbName));
226       // for the type, get the type from the card, or use its default
227       type = aTBContact.getValue(obj.tbName + "Type");
228       if (!type || type === "")
229         type = obj.type;
230       // see the dummy e-mail note below
231       if (obj.tbName === com.gContactSync.dummyEmailName &&
232           com.gContactSync.isDummyEmail(value)) {
233         value = null;
234         type  = null;
235       }
236       com.gContactSync.LOGGER.VERBOSE_LOG("   - " + value + " type: " + type);
237       aGContact.setValue(obj.elementName, obj.index, type, value);
238     }
239     // Birthday can be either YYYY-M-D or --M-D for no year.
240     // TB can have all three, just a day/month, or just a year through the UI
241     var birthDay    = aTBContact.getValue("BirthDay"),
242         birthMonth  = isNaN(parseInt(birthDay, 10)) ?
243                       null : aTBContact.getValue("BirthMonth"),
244         birthdayVal = null;
245     // if the contact has a birth month (and birth day) add it to the contact
246     // from Google
247     if (birthMonth && !isNaN(parseInt(birthMonth, 10))) {
248       var birthYear = parseInt(aTBContact.getValue("BirthYear"), 10);
249       // if the birth year is NaN or 0, use '-'
250       if (!birthYear) {
251         birthYear = "-";
252       }
253       // otherwise pad it to 4 characters
254       else {
255         birthYear = String(birthYear);
256         while (birthYear.length < 4) {
257           birthYear = "0" + birthYear;
258         }
259       }
260       // Pad the birth month to 2 characters
261       birthMonth = String(birthMonth);
262       while (birthMonth.length < 2) {
263         birthMonth = "0" + birthMonth;
264       }
265       // Pad the birth day to 2 characters
266       birthDay = String(birthDay);
267       while (birthDay.length < 2) {
268         birthDay = "0" + birthDay;
269       }
270       // form the birthday string: year-month-day
271       birthdayVal = birthYear + "-" + birthMonth + "-" + birthDay;
272     }
273     com.gContactSync.LOGGER.VERBOSE_LOG(" * Birthday: " + birthdayVal);
274     aGContact.setValue("birthday", 0, null, birthdayVal);
275 
276     // set the extended properties
277     aGContact.removeExtendedProperties();
278     arr = com.gContactSync.Preferences.mExtendedProperties;
279     var props = {};
280     for (i = 0, length = arr.length; i < length; i++) {
281       // add this extended property if it isn't a duplicate
282       if (!props[arr[i]]) {
283         props[arr[i]] = true;
284         value = this.checkValue(aTBContact.getValue(arr[i]));
285         aGContact.setExtendedProperty(arr[i], value);
286       }
287       else {
288         com.gContactSync.LOGGER.LOG_WARNING("Found a duplicate extended property: " +
289                                             arr[i]);
290       }
291     }
292     // If the myContacts pref is set and this contact is new then add the
293     // myContactsName group
294     if (ab.mPrefs.myContacts === "true") {
295       if (isNew && com.gContactSync.Sync.mContactsUrl) {
296         aGContact.setGroups([com.gContactSync.Sync.mContactsUrl]);
297       }
298     }
299     else {
300       // set the groups
301       var groups = [],
302           list;
303       com.gContactSync.LOGGER.VERBOSE_LOG(" * Determining the groups this contact belongs to");
304       for (i in com.gContactSync.Sync.mLists) {
305         list = com.gContactSync.Sync.mLists[i];
306         if (list instanceof com.gContactSync.GMailList) {
307           if (list.hasContact(aTBContact)) {
308             com.gContactSync.LOGGER.VERBOSE_LOG("   - " + list.getName());
309             groups.push(i);
310           }
311         }
312         else {
313           com.gContactSync.LOGGER.LOG_WARNING("   - Found an invalid list: " + i);
314         }
315       }
316       aGContact.setGroups(groups);
317     }
318     // Upload the photo
319     if (com.gContactSync.Preferences.mSyncPrefs.sendPhotos.value) {
320       // Get the profile directory
321       var file = Components.classes["@mozilla.org/file/directory_service;1"]
322                            .getService(Components.interfaces.nsIProperties)
323                            .get("ProfD", Components.interfaces.nsIFile);
324       // Get (or make) the Photos directory
325       file.append("Photos");
326       if (!file.exists() || !file.isDirectory())
327         file.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0777);
328       file.append(aTBContact.getValue("PhotoName"));
329       if (file.exists() && file.isFile()) {
330         aGContact.setPhoto(Components.classes["@mozilla.org/network/io-service;1"]
331                                      .getService(Components.interfaces.nsIIOService)
332                                      .newFileURI(file));
333       }
334       else {
335         aGContact.setPhoto("");
336       }
337     }
338     
339     // Add the phonetic first and last names
340     if (com.gContactSync.Preferences.mSyncPrefs.syncPhoneticNames.value) {
341       aGContact.setAttribute("givenName",
342                              com.gContactSync.gdata.namespaces.GD.url,
343                              0,
344                              "yomi",
345                              aTBContact.getValue("PhoneticFirstName"));
346       aGContact.setAttribute("familyName",
347                              com.gContactSync.gdata.namespaces.GD.url,
348                              0,
349                              "yomi",
350                              aTBContact.getValue("PhoneticLastName"));
351     }
352     
353     return aGContact;
354   },
355   /**
356    * Converts an GContact's Atom/XML representation of a contact to
357    * Thunderbird's address book card format.
358    * @param aGContact {GContact} A GContact object with the contact to convert.
359    * @param aTBContact {TBContact}   An existing card that can be QI'd to
360    *                            Components.interfaces.nsIAbMDBCard if this is
361    *                            before 413260 landed.
362    * @returns {TBContact} The updated TBContact.
363    */
364   makeCard: function ContactConverter_makeCard(aGContact, aTBContact) {
365     if (!aGContact)
366       throw "Invalid aGContact parameter supplied to the 'makeCard' method" +
367             com.gContactSync.StringBundle.getStr("pleaseReport");
368     if (!this.mInitialized)
369       this.init();
370     if (!(aTBContact instanceof com.gContactSync.TBContact)) {
371       throw "Invalid TBContact sent to ContactConverter.makeCard from " +
372             this.caller;
373     }
374     var ab = aTBContact.mAddressBook;
375     if (!(ab instanceof com.gContactSync.AddressBook)) {
376       throw "Invalid TBContact (no mAddressBook) sent to " +
377             "ContactConverter.cardToAtomXML from " + this.caller;
378     }
379     var arr = this.mConverterArr,
380         blankProp = new com.gContactSync.Property("", "");
381     // get the regular properties from the array mConverterArr
382     for (var i = 0, length = arr.length; i < length; i++) {
383       var obj = arr[i],
384           property = aGContact.getValue(obj.elementName, obj.index, obj.type);
385       property = property || blankProp;
386       com.gContactSync.LOGGER.VERBOSE_LOG(obj.tbName + ": '" + property.value +
387                                           "', type: '" + property.type + "'");
388       // Thunderbird has problems with contacts who do not have an e-mail addr
389       // and are in Mailing Lists.  To avoid problems, use a dummy e-mail addr
390       // that is hidden from the user
391       if (obj.tbName === com.gContactSync.dummyEmailName && !property.value) {
392         property.value = com.gContactSync.makeDummyEmail(aGContact);
393         property.type  = "home";
394       }
395       // don't wipe out structured address info
396       if (property.value ||
397            (obj.elementName !== 'street' && obj.elementName !== 'city' &&
398             obj.elementName !== 'region' && obj.elementName !== 'postcode' &&
399             obj.elementName !== 'country')) {
400         aTBContact.setValue(obj.tbName, property.value);
401         // set the type, if it is an attribute with a type
402         if (property.type)
403           aTBContact.setValue(obj.tbName + "Type", property.type);
404       }
405       else {
406         com.gContactSync.LOGGER.VERBOSE_LOG("Going to avoid wiping out " + obj.tbName);
407       }
408     }
409     // get the extended properties
410     arr = com.gContactSync.Preferences.mExtendedProperties;
411     for (i = 0, length = arr.length; i < length; i++) {
412       var value = aGContact.getExtendedProperty(arr[i]);
413       value = value ? value.value : null;
414       aTBContact.setValue(arr[i], value);
415     }
416     
417     // Get the birthday info
418     var bday = aGContact.getValue("birthday", 0, com.gContactSync.gdata.contacts.types.UNTYPED),
419         year  = null,
420         month = null,
421         day   = null;
422     // If it has a birthday...
423     if (bday && bday.value) {
424       com.gContactSync.LOGGER.VERBOSE_LOG(" * Found a birthday value of " + bday.value);
425       // If it consists of all three date elements: YYYY-M-D
426       if (bday.value.indexOf("--") === -1) {
427         arr = bday.value.split("-");
428         year  = arr[0];
429         month = arr[1];
430         day   = arr[2];
431       }
432       // Else it is just a month and day: --M-D
433       else {
434         arr   = bday.value.replace("--", "").split("-");
435         month = arr[0];
436         day   = arr[1];
437       }
438       com.gContactSync.LOGGER.VERBOSE_LOG("  - Year:  " +  year);
439       com.gContactSync.LOGGER.VERBOSE_LOG("  - Month: " +  month);
440       com.gContactSync.LOGGER.VERBOSE_LOG("  - Day:   " +  day);
441     }
442     aTBContact.setValue("BirthYear",  year);
443     aTBContact.setValue("BirthMonth", month);
444     aTBContact.setValue("BirthDay",   day);
445 
446     if (com.gContactSync.Preferences.mSyncPrefs.getPhotos.value) {
447       var info = aGContact.getPhotoInfo();
448       // If the contact has a photo then save it to a local file and update
449       // the related attributes
450       if (info && info.etag &&
451           (file = aGContact.writePhoto(com.gContactSync.Sync.mCurrentAuthToken))) {
452         com.gContactSync.LOGGER.VERBOSE_LOG("Wrote photo...name: " + file.leafName);
453         aTBContact.setValue("PhotoName", file.leafName);
454         aTBContact.setValue("PhotoType", "file");
455         aTBContact.setValue("PhotoURI",
456                             Components.classes["@mozilla.org/network/io-service;1"]
457                                       .getService(Components.interfaces.nsIIOService)
458                                       .newFileURI(file)
459                                       .spec);
460         aTBContact.setValue("PhotoEtag", info.etag);
461       }
462       // If the contact doesn't have a photo then clear the related attributes
463       else {
464         aTBContact.setValue("PhotoName", "");
465         aTBContact.setValue("PhotoType", "");
466         aTBContact.setValue("PhotoURI",  "");
467         aTBContact.setValue("PhotoEtag", "");
468       }
469     }
470     
471     // Add the phonetic first and last names
472     if (com.gContactSync.Preferences.mSyncPrefs.syncPhoneticNames.value) {
473       aTBContact.setValue("PhoneticFirstName",
474                           aGContact.getAttribute("givenName",
475                           com.gContactSync.gdata.namespaces.GD.url,
476                           0,
477                           "yomi"));
478       aTBContact.setValue("PhoneticLastName",
479                           aGContact.getAttribute("familyName",
480                           com.gContactSync.gdata.namespaces.GD.url,
481                           0,
482                           "yomi"));
483     }
484 
485     aTBContact.update();
486     if (ab.mPrefs.syncGroups == "true" && ab.mPrefs.myContacts != "true") {
487       // get the groups after updating the card
488       var groups = aGContact.getValue("groupMembershipInfo"),
489           lists  = com.gContactSync.Sync.mLists,
490           list,
491           group;
492       for (var i in lists) {
493         group = groups[i];
494         list  = lists[i];
495         // delete the card from the list, if necessary
496         if (list.hasContact(aTBContact)) {
497           if (!group) {
498             list.deleteContacts([aTBContact]);
499           }
500           aTBContact.update();
501         }
502         // add the card to the list, if necessary
503         else if (group) {
504           list.addContact(aTBContact);
505         }
506       }
507     }
508   },
509   /**
510    * Check if the given string is null, of length 0, or consists only of spaces
511    * and return null if any of the listed conditions is true.
512    * This function was added to fix Bug 20389: Values with only spaces should be
513    * treated as empty
514    * @param aValue {string} The string to check.
515    * @returns null   - The string is null, of length 0, or consists only of
516                       spaces
517    *         aValue - The string has at least one character that is not a space
518    */
519   checkValue: function ContactConverter_checkValue(aValue) {
520     if (!aValue || !aValue.length) return null;
521     for (var i = 0; i < aValue.length; i++)
522       if (aValue[i] != " ") return aValue;
523     return null;
524   }
525 };
526