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 /**
 42  * Synchronizes a Thunderbird Address Book with Google Contacts.
 43  * @class
 44  */
 45 com.gContactSync.Sync = {
 46   /** Google contacts that should be deleted */
 47   mContactsToDelete: [],
 48   /** New contacts to add to Google */
 49   mContactsToAdd:    [],
 50   /** Contacts to update */
 51   mContactsToUpdate: [],
 52   /** Groups to delete */
 53   mGroupsToDelete:   [],
 54   /** Groups to add */
 55   mGroupsToAdd:      [],
 56   /** Groups to update */
 57   mGroupsToUpdate:   [],
 58   /** Groups to add (URIs) */
 59   mGroupsToAddURI:   [],
 60   /** The current authentication token */
 61   mCurrentAuthToken: {},
 62   /** The current username */
 63   mCurrentUsername:  {},
 64   /** The current address book being synchronized */
 65   mCurrentAb:        {},
 66   /** Synchronized address book */
 67   mAddressBooks:     [],
 68   /** The index of the AB being synced */
 69   mIndex:            0,
 70   /** The URI of a photo to be added to the newly created Google contact */
 71   mNewPhotoURI:      {},
 72   /** Temporarily set to true when a backup is necessary for this account */
 73   mBackup:           false,
 74   /** Temporarily set to true when the first backup is necessary */
 75   mFirstBackup:      false,
 76   /** Commands to execute when offline during an HTTP Request */
 77   mOfflineFunction: function Sync_offlineFunc(httpReq) {
 78     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr('offlineStatusText')); 
 79     com.gContactSync.Sync.finish(com.gContactSync.StringBundle.getStr('offlineStatusText'));
 80   },
 81   /** True if a synchronization is scheduled */
 82   mSyncScheduled: false,
 83   /** used to store groups for the account being synchronized */
 84   mGroups:        {},
 85   /** stores the mail lists in the directory being synchronized */
 86   mLists:         {},
 87   /** override for the contact feed URL.  Intended for syncing one group only */
 88   mContactsUrl:   null,
 89   /**
 90    * Performs the first steps of the sync process.
 91    */
 92   begin: function Sync_begin() {
 93     if (!com.gContactSync.gdata.isAuthValid()) {
 94       com.gContactSync.alert(com.gContactSync.StringBundle.getStr("pleaseAuth"));
 95       return;
 96    }
 97     // quit if still syncing.
 98     if (com.gContactSync.Preferences.mSyncPrefs.synchronizing.value) {
 99       return;
100     }
101     com.gContactSync.Sync.mSyncScheduled = false;
102     com.gContactSync.Preferences.setSyncPref("synchronizing", true);
103     com.gContactSync.Sync.mBackup        = false;
104     com.gContactSync.LOGGER.mErrorCount  = 0; // reset the error count
105     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("syncing"));
106     com.gContactSync.Sync.mIndex         = 0;
107     com.gContactSync.Sync.mAddressBooks  = com.gContactSync.GAbManager.getSyncedAddressBooks(true);
108     com.gContactSync.Sync.syncNextUser();
109   },
110   /**
111    * Synchronizes the next address book in com.gContactSync.Sync.mAddressBooks.
112    * If all ABs were synchronized, then this continues with com.gContactSync.Sync.finish();
113    */
114   syncNextUser: function Sync_syncNextUser() {
115     var obj = com.gContactSync.Sync.mAddressBooks[com.gContactSync.Sync.mIndex++];
116     if (!obj) {
117       com.gContactSync.Sync.finish();
118       return;
119     }
120     // make sure the user doesn't have to restart TB
121     if (com.gContactSync.Preferences.mSyncPrefs.needRestart.value) {
122       var restartStr = com.gContactSync.StringBundle.getStr("pleaseRestart");
123       com.gContactSync.alert(restartStr);
124       com.gContactSync.Overlay.setStatusBarText(restartStr);
125       return;
126     }
127     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("syncing"));
128     com.gContactSync.Sync.mCurrentUsername = obj.username;
129     com.gContactSync.LOGGER.LOG("Starting Synchronization for " + com.gContactSync.Sync.mCurrentUsername +
130                                 " at: " + Date() + "\n");
131     com.gContactSync.Sync.mCurrentAb        = obj.ab;
132     com.gContactSync.Sync.mCurrentAuthToken = com.gContactSync.LoginManager.getAuthTokens()[com.gContactSync.Sync.mCurrentUsername];
133     com.gContactSync.Sync.mContactsUrl      = null;
134     com.gContactSync.Sync.mBackup           = false;
135     com.gContactSync.LOGGER.VERBOSE_LOG("Found Address Book with name: " +
136                                         com.gContactSync.Sync.mCurrentAb.mDirectory.dirName +
137                                         "\n - URI: " + com.gContactSync.Sync.mCurrentAb.mURI +
138                                         "\n - Pref ID: " + com.gContactSync.Sync.mCurrentAb.getPrefId());
139     if (com.gContactSync.Sync.mCurrentAb.mPrefs.Disabled === "true") {
140       com.gContactSync.LOGGER.LOG("*** NOTE: Synchronization was disabled for this address book ***");
141       com.gContactSync.Sync.mCurrentAb = null;
142       com.gContactSync.Sync.syncNextUser();
143       return;
144     }
145     // If an authentication token cannot be found for this username then
146     // offer to let the user login with that account
147     if (!com.gContactSync.Sync.mCurrentAuthToken) {
148       com.gContactSync.LOGGER.LOG_WARNING("Unable to find the auth token for: " +
149                                           com.gContactSync.Sync.mCurrentUsername);
150       if (com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("noTokenFound") +
151                   ": " + com.gContactSync.Sync.mCurrentUsername +
152                   "\n" + com.gContactSync.StringBundle.getStr("ab") +
153                   ": " + com.gContactSync.Sync.mCurrentAb.getName())) {
154         // Now let the user login
155         var prompt   = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
156                                  .getService(Components.interfaces.nsIPromptService)
157                                  .promptUsernameAndPassword,
158             username = {value: com.gContactSync.Sync.mCurrentUsername},
159             password = {},
160         // opens a username/password prompt
161             ok = prompt(window, com.gContactSync.StringBundle.getStr("loginTitle"),
162                         com.gContactSync.StringBundle.getStr("loginText"), username, password, null,
163                         {value: false});
164         if (!ok) {
165           com.gContactSync.Sync.syncNextUser();
166           return;
167         }
168         // Decrement the index so Sync.syncNextUser runs on this AB again
169         com.gContactSync.Sync.mIndex--;
170         // This is a primitive way of validating an e-mail address, but Google takes
171         // care of the rest.  It seems to allow getting an auth token w/ only the
172         // username, but returns an error when trying to do anything w/ that token
173         // so this makes sure it is a full e-mail address.
174         if (username.value.indexOf("@") < 1) {
175           com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("invalidEmail"));
176           com.gContactSync.Sync.syncNextUser();
177           return;
178         }
179         // fix the username before authenticating
180         username.value = com.gContactSync.fixUsername(username.value);
181         var body    = com.gContactSync.gdata.makeAuthBody(username.value, password.value);
182         var httpReq = new com.gContactSync.GHttpRequest("authenticate", null, null, body);
183         // if it succeeds and Google returns the auth token, store it and then start
184         // a new sync
185         httpReq.mOnSuccess = function reauth_onSuccess(httpReq) {
186           com.gContactSync.LoginManager.addAuthToken(username.value,
187                                                      'GoogleLogin' +
188                                                      httpReq.responseText.split("\n")[2]);
189           com.gContactSync.Sync.syncNextUser();
190         };
191         // if it fails, alert the user and prompt them to try again
192         httpReq.mOnError   = function reauth_onError(httpReq) {
193           com.gContactSync.alertError(com.gContactSync.StringBundle.getStr('authErr'));
194           com.gContactSync.LOGGER.LOG_ERROR('Authentication Error - ' +
195                                             httpReq.status,
196                                             httpReq.responseText);
197           com.gContactSync.Sync.syncNextUser();
198         };
199         // if the user is offline, alert them and quit
200         httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
201         httpReq.send();
202       }
203       else
204         com.gContactSync.Sync.syncNextUser();
205       return;
206     }
207     var lastBackup = parseInt(obj.ab.mPrefs.lastBackup, 10),
208         interval   = com.gContactSync.Preferences.mSyncPrefs.backupInterval.value * 24 * 3600 * 1000,
209         prefix     = "";
210     com.gContactSync.LOGGER.VERBOSE_LOG(" - Last backup was at " + lastBackup +
211                                         ", interval is " + interval);
212     this.mFirstBackup = !lastBackup && interval >= 0;
213     this.mBackup      = this.mFirstBackup || interval >= 0 && new Date().getTime() - lastBackup > interval;
214     prefix = this.mFirstBackup ? "init_" : "";
215     if (this.mBackup) {
216       com.gContactSync.GAbManager.backupAB(com.gContactSync.Sync.mCurrentAb,
217                                            prefix,
218                                            ".bak");
219     }
220     // getGroups must be called if the myContacts pref is set so it can find the
221     // proper group URL
222     if (com.gContactSync.Sync.mCurrentAb.mPrefs.syncGroups === "true" ||
223         (com.gContactSync.Sync.mCurrentAb.mPrefs.myContacts !== "false" &&
224          com.gContactSync.Sync.mCurrentAb.mPrefs.myContactsName !== "false")) {
225       com.gContactSync.Sync.getGroups();
226     }
227     else {
228       com.gContactSync.Sync.getContacts();
229     }
230   },
231   /**
232    * Sends an HTTP Request to Google for a feed of all of the user's groups.
233    * Calls com.gContactSync.Sync.begin() when there is a successful response on an error other
234    * than offline.
235    */
236   getGroups: function Sync_getGroups() {
237     com.gContactSync.LOGGER.LOG("***Beginning Group - Mail List Synchronization***");
238     var httpReq = new com.gContactSync.GHttpRequest("getGroups",
239                                                     com.gContactSync.Sync.mCurrentAuthToken,
240                                                     null,
241                                                     null,
242                                                     com.gContactSync.Sync.mCurrentUsername);
243     httpReq.mOnSuccess = function getGroupsSuccess(httpReq) {
244       com.gContactSync.LOGGER.VERBOSE_LOG(com.gContactSync.serializeFromText(httpReq.responseText));
245       com.gContactSync.Sync.syncGroups(httpReq.responseXML);
246     };
247     httpReq.mOnError   = function getGroupsError(httpReq) {
248       com.gContactSync.LOGGER.LOG_ERROR(httpReq.responseText);
249       // if there is an error, try to sync w/o groups                   
250       com.gContactSync.Sync.begin();
251     };
252     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
253     httpReq.send();
254   },
255   /**
256    * Sends an HTTP Request to Google for a feed of all the user's contacts.
257    * Calls com.gContactSync.Sync.sync with the response if successful or com.gContactSync.Sync.syncNextUser with the
258    * error.
259    */
260   getContacts: function Sync_getContacts() {
261     com.gContactSync.LOGGER.LOG("***Beginning Contact Synchronization***");
262     var httpReq;
263     if (com.gContactSync.Sync.mContactsUrl) {
264       httpReq = new com.gContactSync.GHttpRequest("getFromGroup",
265                                                   com.gContactSync.Sync.mCurrentAuthToken,
266                                                   null,
267                                                   null,
268                                                   com.gContactSync.Sync.mCurrentUsername, com.gContactSync.Sync.mContactsUrl);
269     }
270     else {
271       httpReq = new com.gContactSync.GHttpRequest("getAll",
272                                                   com.gContactSync.Sync.mCurrentAuthToken,
273                                                   null,
274                                                   null,
275                                                   com.gContactSync.Sync.mCurrentUsername);
276     }
277     httpReq.mOnSuccess = function getContactsSuccess(httpReq) {
278       // com.gContactSync.serializeFromText does not do anything if verbose
279       // logging is disabled so the serialization won't waste time
280       var backup      = com.gContactSync.Sync.mBackup,
281           firstBackup = com.gContactSync.Sync.mFirstBackup,
282           feed        = com.gContactSync.serializeFromText(httpReq.responseText,
283                                                       backup);
284       com.gContactSync.LOGGER.VERBOSE_LOG(feed);
285       if (backup) {
286         com.gContactSync.gdata.backupFeed(feed,
287                                           com.gContactSync.Sync.mCurrentUsername,
288                                           (firstBackup ? "init_" : ""),
289                                           ".bak");
290       }
291       com.gContactSync.Sync.sync2(httpReq.responseXML);
292     };
293     httpReq.mOnError   = function getContactsError(httpReq) {
294       com.gContactSync.LOGGER.LOG_ERROR('Error while getting all contacts',
295                                         httpReq.responseText);
296       com.gContactSync.Sync.syncNextUser(httpReq.responseText);
297     };
298     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
299     httpReq.send();
300   },
301   /**
302    * Completes the synchronization process by writing the finish time to a file,
303    * writing the sync details to a different file, scheduling another sync, and
304    * writes the completion status to the status bar.
305    * 
306    * @param aError     {string}   Optional.  A string containing the error message.
307    * @param aStartOver {boolean} Also optional.  True if the sync should be restarted.
308    */
309   finish: function Sync_finish(aError, aStartOver) {
310     if (aError)
311       com.gContactSync.LOGGER.LOG_ERROR("Error during sync", aError);
312     if (com.gContactSync.LOGGER.mErrorCount > 0) {
313       // if there was an error, display the error message unless the user is
314       // offline
315       if (com.gContactSync.Overlay.getStatusBarText() !== aError)
316         com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("errDuringSync"));
317     }
318     else {
319       com.gContactSync.Overlay.writeTimeToStatusBar();
320       com.gContactSync.LOGGER.LOG("Finished Synchronization at: " + Date());
321     }
322     // reset some variables
323     com.gContactSync.ContactConverter.mCurrentCard = {};
324     com.gContactSync.Preferences.setSyncPref("synchronizing", false);
325     com.gContactSync.Sync.mCurrentAb               = {};
326     com.gContactSync.Sync.mContactsUrl             = null;
327     com.gContactSync.Sync.mCurrentUsername         = {};
328     com.gContactSync.Sync.mCurrentAuthToken        = {};
329     // refresh the ab results pane
330     // https://www.mozdev.org/bugs/show_bug.cgi?id=19733
331     try {
332       if (SetAbView !== undefined) {
333         SetAbView(GetSelectedDirectory(), false);
334       }
335       
336       // select the first card, if any
337       if (gAbView && gAbView.getCardFromRow(0))
338         SelectFirstCard();
339       }
340     catch (e) {}
341     // start over, if necessary, or schedule the next synchronization
342     if (aStartOver)
343       com.gContactSync.Sync.begin();
344     else
345       com.gContactSync.Sync.schedule(com.gContactSync.Preferences.mSyncPrefs.refreshInterval.value * 60000);
346   },
347   /**
348    * Does the actual synchronization of contacts and modifies the AB as it goes.
349    * Initializes arrays of Google contacts to add, remove, or update.
350    * @param aAtom {XML} The ATOM/XML feed of contacts.
351    */
352   sync2: function Sync_sync2(aAtom) {
353     // get the address book
354     var ab = com.gContactSync.Sync.mCurrentAb,
355         // get all the contacts from the feed and the cards from the address book
356         googleContacts = aAtom.getElementsByTagName('entry'),
357         abCards = ab.getAllContacts(),
358         // get and log the last sync time (milliseconds since 1970 UTC)
359         lastSync = parseInt(ab.mPrefs.lastSync, 10),
360         cardsToDelete = [],
361         maxContacts = com.gContactSync.Preferences.mSyncPrefs.maxContacts.value,
362         // if there are more contacts than returned, increase the pref
363         newMax;
364     if (isNaN(lastSync)) {
365       com.gContactSync.LOGGER.LOG_WARNING("lastSync was NaN, setting to 0");
366       lastSync = 0;
367     }
368     // mark the AB as not having been reset if it gets this far
369     com.gContactSync.Sync.mCurrentAb.savePref("reset", false);
370     // have to update the lists or TB 2 won't work properly
371     com.gContactSync.Sync.mLists = ab.getAllLists();
372     com.gContactSync.LOGGER.LOG("Last sync was at: " + lastSync);
373     if ((newMax = com.gContactSync.gdata.contacts.getNumberOfContacts(aAtom)) >= maxContacts.value) {
374       com.gContactSync.Preferences.setPref(com.gContactSync.Preferences.mSyncBranch, maxContacts.label,
375                                            maxContacts.type, newMax + 50);
376       com.gContactSync.Sync.finish("Max Contacts too low...resynchronizing", true);
377       return;
378     }
379     com.gContactSync.Sync.mContactsToAdd    = [];
380     com.gContactSync.Sync.mContactsToDelete = [];
381     com.gContactSync.Sync.mContactsToUpdate = [];
382     var gContact,
383      // get the strings outside of the loop so they are only found once
384         found       = " * Found a match Last Modified Dates:",
385         bothChanged = " * Conflict detected: the contact has been updated in " +
386                       "both Google and Thunderbird",
387         bothGoogle  = " * The contact from Google will be updated",
388         bothTB      = " * The card from Thunderbird will be updated",
389         gContacts   = {};
390     // Step 1: get all contacts from Google into GContact objects in an object
391     // keyed by ID.
392     for (var i = 0, length = googleContacts.length; i < length; i++) {
393       gContact               = new com.gContactSync.GContact(googleContacts[i]);
394       gContact.lastModified  = gContact.getLastModifiedDate();
395       gContact.id            = gContact.getID(true);
396       gContacts[gContact.id] = gContact;
397     }
398     // re-initialize the contact converter (in case a pref changed)
399     com.gContactSync.ContactConverter.init();
400     // Step 2: iterate through TB Contacts and check for matches
401     for (i = 0, length = abCards.length; i < length; i++) {
402       var tbContact  = abCards[i],
403           id         = tbContact.getID(),
404           tbCardDate = tbContact.getValue("LastModifiedDate");
405       com.gContactSync.LOGGER.LOG(tbContact.getName() + ": " + id + " - " + tbCardDate);
406       tbContact.id = id;
407       // no ID = new contact
408       if (!id) {
409         if (ab.mPrefs.readOnly === "true") {
410           com.gContactSync.LOGGER.LOG(" * The contact is new. " +
411                                       "Ignoring since read-only mode is on.");
412         }
413         else {
414           com.gContactSync.LOGGER.LOG(" * This contact is new and will be added to Google.");
415           com.gContactSync.Sync.mContactsToAdd.push(tbContact);
416         }
417       }
418       // if there is a matching Google Contact
419       else if (gContacts[id]) {
420         gContact   = gContacts[id];
421         // remove it from gContacts
422         gContacts[id]  = null;
423         // note that this returns 0 if readOnly is set
424             gCardDate  = ab.mPrefs.writeOnly !== "true" ? gContact.lastModified : 0;
425         // 4 options
426         // if both were updated
427         com.gContactSync.LOGGER.LOG(found + "  -  " + gCardDate + " - " + tbCardDate);
428         com.gContactSync.LOGGER.VERBOSE_LOG(" * Google ID: " + id);
429         // If there is a conflict, looks at the updateGoogleInConflicts
430         // preference and updates Google if it's true, or Thunderbird if false
431         if (gCardDate > lastSync && tbCardDate > lastSync / 1000) {
432           com.gContactSync.LOGGER.LOG(bothChanged);
433           if (ab.mPrefs.writeOnly  === "true" || ab.mPrefs.updateGoogleInConflicts === "true") {
434             com.gContactSync.LOGGER.LOG(bothGoogle);
435             var toUpdate = {};
436             toUpdate.gContact = gContact;
437             toUpdate.abCard   = tbContact;
438             com.gContactSync.Sync.mContactsToUpdate.push(toUpdate);
439           }
440           // update Thunderbird if writeOnly is off and updateGoogle is off
441           else {
442             com.gContactSync.LOGGER.LOG(bothTB);
443             com.gContactSync.ContactConverter.makeCard(gContact, tbContact);
444           }
445         }
446         // if the contact from google is newer update the TB card
447         else if (gCardDate > lastSync) {
448           com.gContactSync.LOGGER.LOG(" * The contact from Google is newer...Updating the" +
449                                       " contact from Thunderbird");
450           com.gContactSync.ContactConverter.makeCard(gContact, tbContact);
451         }
452         // if the TB card is newer update Google
453         else if (tbCardDate > lastSync / 1000) {
454           com.gContactSync.LOGGER.LOG(" * The contact from Thunderbird is newer...Updating the" +
455                                       " contact from Google");
456           var toUpdate = {};
457           toUpdate.gContact = gContact;
458           toUpdate.abCard   = tbContact;
459           com.gContactSync.Sync.mContactsToUpdate.push(toUpdate);
460         }
461         // otherwise nothing needs to be done
462         else
463           com.gContactSync.LOGGER.LOG(" * Neither contact has changed");
464       }
465       // if there isn't a match, but the card is new, add it to Google
466       else if (tbContact.getValue("LastModifiedDate") > lastSync / 1000 ||
467                isNaN(lastSync))
468         com.gContactSync.Sync.mContactsToAdd.push(tbContact);
469       // otherwise, delete the contact from the address book
470       else
471         cardsToDelete.push(tbContact);
472     }
473     // STEP 3: Check for old Google contacts to delete and new contacts to add to TB
474     com.gContactSync.LOGGER.LOG("**Looking for unmatched Google contacts**");
475     for (var id in gContacts) {
476       var gContact = gContacts[id];
477       if (gContact) {
478         var gCardDate = ab.mPrefs.writeOnly != "true" ? gContact.lastModified : 1;
479         com.gContactSync.LOGGER.LOG(gContact.getName() + " - " + gCardDate +
480                                     "\n" + id);
481         if (gCardDate > lastSync || isNaN(lastSync)) {
482           com.gContactSync.LOGGER.LOG(" * The contact is new and will be added to Thunderbird");
483           var newCard = ab.newContact();
484           com.gContactSync.ContactConverter.makeCard(gContact, newCard);
485         }
486         else if (ab.mPrefs.readOnly != "true") {
487           com.gContactSync.LOGGER.LOG(" * The contact is old will be deleted");
488           com.gContactSync.Sync.mContactsToDelete.push(gContact);
489         }
490         else {
491           com.gContactSync.LOGGER.LOG (" * The contact was deleted in Thunderbird.  " +
492                                        "Ignoring since read-only mode is on.");
493         }
494       }
495     }
496     var threshold = com.gContactSync.Preferences.mSyncPrefs
497                                                 .confirmDeleteThreshold.value;
498     // Request permission from the user to delete > threshold contacts from a
499     // single source
500     // If the user clicks Cancel the AB is disabled
501     if (threshold > -1 &&
502           (cardsToDelete.length >= threshold ||
503            com.gContactSync.Sync.mContactsToDelete.length >= threshold) &&
504           !com.gContactSync.Sync.requestDeletePermission(cardsToDelete.length,
505                                                        com.gContactSync.Sync.mContactsToDelete.length)) {
506         com.gContactSync.Sync.syncNextUser();
507         return;
508     }
509     // delete the old contacts from Thunderbird
510     if (cardsToDelete.length > 0) {
511       ab.deleteContacts(cardsToDelete);
512     }
513 
514     com.gContactSync.LOGGER.LOG("***Deleting contacts from Google***");
515     // delete contacts from Google
516     com.gContactSync.Sync.processDeleteQueue();
517   },
518   /**
519    * Shows a confirmation dialog asking the user to give gContactSync permission
520    * to delete the specified number of contacts from Google and Thunderbird.
521    * If the user clicks Cancel then synchronization with the current address
522    * book is disabled.
523    * @param {int} The number of contacts about to be deleted from Thunderbird.
524    * @param {int} The number of contacts about to be deleted from Google.
525    * @returns {boolean} True if the user clicked OK, false if Cancel.
526    */
527   requestDeletePermission: function Sync_requestDeletePermission(aNumTB, aNumGoogle) {
528     var warning = com.gContactSync.StringBundle.getStr("confirmDelete1") +
529                   " '" + com.gContactSync.Sync.mCurrentAb.getName() + "'" +
530                   "\nThunderbird: " + aNumTB +
531                   "\nGoogle: "      + aNumGoogle +
532                   "\n" + com.gContactSync.StringBundle.getStr("confirmDelete2");
533     com.gContactSync.LOGGER.LOG("Requesting permission to delete " +
534                                 "TB: " + aNumTB + ", Google: " + aNumGoogle +
535                                 " contacts...");
536     if (!com.gContactSync.confirm(warning)) {
537       com.gContactSync.LOGGER.LOG(" * Permission denied, disabling AB");
538       com.gContactSync.Sync.mCurrentAb.savePref("Disabled", true);
539       com.gContactSync.alert(com.gContactSync.StringBundle.getStr("deleteCancel"));
540       return false;
541     }
542     com.gContactSync.LOGGER.LOG(" * Permission granted");
543     return true;
544   },
545   /**
546    * Deletes all contacts from Google included in the mContactsToDelete
547    * array one at a time to avoid timing conflicts. Calls
548    * com.gContactSync.Sync.processAddQueue() when finished.
549    */
550   processDeleteQueue: function Sync_processDeleteQueue() {
551     var ab = com.gContactSync.Sync.mCurrentAb;
552     if (!com.gContactSync.Sync.mContactsToDelete
553         || com.gContactSync.Sync.mContactsToDelete.length == 0
554         || ab.mPrefs.readOnly == "true") {
555       com.gContactSync.LOGGER.LOG("***Adding contacts to Google***");
556       com.gContactSync.Sync.processAddQueue();
557       return;
558     }
559     // TODO if com.gContactSync.Sync.mContactsUrl is set should the contact just
560     // be removed from that group or completely removed?
561     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("deleting") + " " +
562                                               com.gContactSync.Sync.mContactsToDelete.length + " " +
563                                               com.gContactSync.StringBundle.getStr("remaining"));
564     var contact = com.gContactSync.Sync.mContactsToDelete.shift();
565     var editURL = contact.getValue("EditURL").value;
566     com.gContactSync.LOGGER.LOG(" * " + contact.getName() + "  -  " + editURL);
567 
568     var httpReq = new com.gContactSync.GHttpRequest("delete",
569                                                     com.gContactSync.Sync.mCurrentAuthToken,
570                                                     editURL, null,
571                                                     com.gContactSync.Sync.mCurrentUsername);
572     httpReq.addHeaderItem("If-Match", "*");
573     httpReq.mOnSuccess = com.gContactSync.Sync.processDeleteQueue;
574     httpReq.mOnError   = function processDeleteError(httpReq) {
575       com.gContactSync.LOGGER.LOG_ERROR('Error while deleting contact',
576                                         httpReq.responseText);
577       com.gContactSync.Sync.processDeleteQueue();
578     };
579     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
580     httpReq.send();
581   },
582   /**
583    * Adds all cards to Google included in the mContactsToAdd array one at a 
584    * time to avoid timing conflicts.  Calls
585    * com.gContactSync.Sync.processUpdateQueue() when finished.
586    */
587   processAddQueue: function Sync_processAddQueue() {
588     var ab = com.gContactSync.Sync.mCurrentAb;
589     // if all contacts were added then update all necessary contacts
590     if (!com.gContactSync.Sync.mContactsToAdd
591         || com.gContactSync.Sync.mContactsToAdd.length == 0
592         || ab.mPrefs.readOnly == "true") {
593       com.gContactSync.LOGGER.LOG("***Updating contacts from Google***");
594       com.gContactSync.Sync.processUpdateQueue();
595       return;
596     }
597     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("adding") + " " +
598                                               com.gContactSync.Sync.mContactsToAdd.length + " " +
599                                               com.gContactSync.StringBundle.getStr("remaining"));
600     var cardToAdd = com.gContactSync.Sync.mContactsToAdd.shift();
601     com.gContactSync.LOGGER.LOG("\n" + cardToAdd.getName());
602     // get the XML representation of the card
603     // NOTE: cardToAtomXML adds the contact to the current group, if any
604     var gcontact = com.gContactSync.ContactConverter.cardToAtomXML(cardToAdd);
605     var xml      = gcontact.xml;
606     var string   = com.gContactSync.serialize(xml);
607     if (com.gContactSync.Preferences.mSyncPrefs.verboseLog.value)
608       com.gContactSync.LOGGER.LOG(" * XML of contact being added:\n" + string + "\n");
609     var httpReq = new com.gContactSync.GHttpRequest("add",
610                                                     com.gContactSync.Sync.mCurrentAuthToken,
611                                                     null,
612                                                     string,
613                                                     com.gContactSync.Sync.mCurrentUsername);
614     this.mNewPhotoURI = com.gContactSync.Preferences.mSyncPrefs.sendPhotos ?
615                           gcontact.mNewPhotoURI : null;
616     /* When the contact is successfully created:
617      *  1. Get the card from which the contact was made
618      *  2. Get a GContact object for the new contact
619      *  3. Set the card's GoogleID attribute to match the new contact's ID
620      *  4. Update the card in the address book
621      *  5. Set the new contact's photo, if necessary
622      *  6. Call this method again
623      */
624     var onCreated = function contactCreated(httpReq) {
625       var ab       = com.gContactSync.Sync.mCurrentAb,
626           contact  = com.gContactSync.ContactConverter.mCurrentCard,
627           gcontact = new com.gContactSync.GContact(httpReq.responseXML);
628       contact.setValue('GoogleID', gcontact.getID(true));
629       contact.update();
630       // if photos are allowed to be uploaded to Google then do so
631       if (com.gContactSync.Preferences.mSyncPrefs.sendPhotos) {
632         gcontact.setPhoto(com.gContactSync.Sync.mNewPhotoURI);
633       }
634       // reset the new photo URI variable
635       com.gContactSync.Sync.mNewPhotoURI = null;
636       com.gContactSync.Sync.processAddQueue();
637     }
638     httpReq.mOnCreated = onCreated;
639     httpReq.mOnError   = function contactCreatedError(httpReq) {
640       com.gContactSync.LOGGER.LOG_ERROR('Error while adding contact',
641                                         httpReq.responseText);
642       com.gContactSync.Sync.processAddQueue();
643     };
644     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
645     httpReq.send();
646   },
647   /**
648    * Updates all cards to Google included in the mContactsToUpdate array one at
649    * a time to avoid timing conflicts.  Calls
650    * com.gContactSync.Sync.syncNextUser() when done if there is at least one
651    * more AB to sync, otherwise calls com.gContactSync.Sync.finish().
652    */
653   processUpdateQueue: function Sync_processUpdateQueue() {
654     var ab = com.gContactSync.Sync.mCurrentAb;
655     if (!com.gContactSync.Sync.mContactsToUpdate
656         || com.gContactSync.Sync.mContactsToUpdate.length == 0
657         || ab.mPrefs.readOnly == "true") {
658       // set the previous address book's last sync date (if it exists)
659       if (com.gContactSync.Sync.mCurrentAb &&
660           com.gContactSync.Sync.mCurrentAb.setLastSyncDate) {
661         com.gContactSync.Sync.mCurrentAb.setLastSyncDate((new Date()).getTime());
662       }
663       if (com.gContactSync.Sync.mAddressBooks[com.gContactSync.Sync.mIndex]) {
664         var delay = com.gContactSync.Preferences.mSyncPrefs.accountDelay.value;
665         com.gContactSync.LOGGER.LOG("**About to wait " + delay +
666                                     " ms before synchronizing the next account**");
667         com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("waiting"));
668         setTimeout(com.gContactSync.Sync.syncNextUser, delay);
669       }
670       else {
671         com.gContactSync.Sync.finish();
672       }
673       return;
674     }
675     com.gContactSync.Overlay.setStatusBarText(com.gContactSync.StringBundle.getStr("updating") + " " +
676                                               com.gContactSync.Sync.mContactsToUpdate.length + " " +
677                                               com.gContactSync.StringBundle.getStr("remaining"));
678     var obj      = com.gContactSync.Sync.mContactsToUpdate.shift();
679     var gContact = obj.gContact;
680     var abCard   = obj.abCard;
681 
682     var editURL = gContact.getValue("EditURL").value;
683     com.gContactSync.LOGGER.LOG("\nUpdating " + gContact.getName());
684     var xml = com.gContactSync.ContactConverter.cardToAtomXML(abCard, gContact).xml;
685 
686     var string = com.gContactSync.serialize(xml);
687     if (com.gContactSync.Preferences.mSyncPrefs.verboseLog.value)
688       com.gContactSync.LOGGER.LOG(" * XML of contact being updated:\n" + string + "\n");
689     var httpReq = new com.gContactSync.GHttpRequest("update",
690                                                     com.gContactSync.Sync.mCurrentAuthToken,
691                                                     editURL,
692                                                     string,
693                                                     com.gContactSync.Sync.mCurrentUsername);
694     httpReq.addHeaderItem("If-Match", "*");
695     httpReq.mOnSuccess = com.gContactSync.Sync.processUpdateQueue;
696     httpReq.mOnError   = function processUpdateError(httpReq) {
697       com.gContactSync.LOGGER.LOG_ERROR('Error while updating contact',
698                                         httpReq.responseText);
699       com.gContactSync.Sync.processUpdateQueue();
700     };
701     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
702     httpReq.send();
703   },
704   /**
705    * Syncs all contact groups with mailing lists.
706    * @param aAtom {XML} The ATOM/XML feed of Groups.
707    */
708   syncGroups: function Sync_syncGroups(aAtom) {
709     // reset the groups object
710     com.gContactSync.Sync.mGroups         = {};
711     com.gContactSync.Sync.mLists          = {};
712     com.gContactSync.Sync.mGroupsToAdd    = [];
713     com.gContactSync.Sync.mGroupsToDelete = [];
714     com.gContactSync.Sync.mGroupsToUpdate = [];
715     // if there wasn't an error, setup groups
716     if (aAtom) {
717       var ab         = com.gContactSync.Sync.mCurrentAb;
718       var ns         = com.gContactSync.gdata.namespaces.ATOM;
719       var lastSync   = parseInt(ab.mPrefs.lastSync, 10);
720       var myContacts = ab.mPrefs.myContacts == "true" && ab.mPrefs.myContactsName;
721       var arr        = aAtom.getElementsByTagNameNS(ns.url, "entry");
722       var noCatch    = false;
723       // get the mailing lists if not only synchronizing my contacts
724       if (!myContacts) {
725         com.gContactSync.LOGGER.VERBOSE_LOG("***Getting all mailing lists***");
726         com.gContactSync.Sync.mLists = ab.getAllLists(true);
727         com.gContactSync.LOGGER.VERBOSE_LOG("***Getting all contact groups***");
728         for (var i = 0; i < arr.length; i++) {
729           try {
730             var group = new com.gContactSync.Group(arr[i]);
731             // add the ID to mGroups by making a new property with the ID as the
732             // name and the title as the value for easy lookup for contacts
733             var id = group.getID();
734             var title = group.getTitle();
735             var modifiedDate = group.getLastModifiedDate();
736             com.gContactSync.LOGGER.LOG(" * " + title + " - " + id +
737                                         " last modified: " + modifiedDate);
738             var list = com.gContactSync.Sync.mLists[id];
739             com.gContactSync.Sync.mGroups[id] = group;
740             if (modifiedDate < lastSync) { // it's an old group
741               if (list) {
742                 list.matched = true;
743                 // if the name is different, update the group's title
744                 var listName = list.getName();
745                 com.gContactSync.LOGGER.LOG("  - Matched with mailing list " + listName);
746                 if (listName != title) {
747                   // You cannot rename system groups...so change the name back
748                   // In the future system groups will be localized, so this
749                   // must be ignored.
750                   if (group.isSystemGroup()) {
751                     // If write-only is on then ignore the name change
752                     if (ab.mPrefs.writeOnly != "true")
753                       list.setName(title);
754                     com.gContactSync.LOGGER.LOG_WARNING("  - A system group was renamed in Thunderbird");
755                   }
756                   else if (ab.mPrefs.readOnly == "true") {
757                     com.gContactSync.LOGGER.LOG(" - The mailing list's name has changed.  " +
758                                                 "Ignoring since read-only mode is on.");
759                   }
760                   else {
761                     com.gContactSync.LOGGER.LOG("  - Going to rename the group to " + listName);
762                     group.setTitle(listName);
763                     com.gContactSync.Sync.mGroupsToUpdate.push(group);
764                   }
765                 }
766               }
767               else {
768                 if (ab.mPrefs.readOnly == "true") {
769                   com.gContactSync.LOGGER.LOG(" - A mailing list was deleted.  " +
770                                               "Ignoring since read-only mode is on.");
771                 }
772                 else {
773                   // System groups cannot be deleted.
774                   // This would be difficult to recover from, so stop
775                   // synchronization and reset the AB
776                   if (group.isSystemGroup()) {
777                     noCatch = true; // don't catch this error
778                     com.gContactSync.LOGGER.LOG_ERROR("  - A system group was deleted from Thunderbird");
779                     var restartStr = com.gContactSync.StringBundle.getStr("pleaseRestart");
780                     if (com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("resetConfirm"))) {
781                       ab.reset();
782                       com.gContactSync.Overlay.setStatusBarText(restartStr);
783                       com.gContactSync.alert(restartStr);
784                       com.gContactSync.Preferences.setSyncPref("needRestart", true);
785                     }
786                     // Throw an error to stop the sync
787                     throw "A system group was deleted from Thunderbird";                  
788                   }
789                   else {
790                     com.gContactSync.Sync.mGroupsToDelete.push(group);
791                     com.gContactSync.LOGGER.LOG("  - Didn't find a matching mail list.  It will be deleted");
792                   }
793                 }
794               }
795             }
796             else { // it is new or updated
797               if (list) { // the group has been updated
798                 com.gContactSync.LOGGER.LOG("  - Matched with mailing list " + listName);
799                 // if the name changed, update the mail list's name
800                 if (list.getName() != title) {
801                   if (ab.mPrefs.writeOnly == "true") {
802                     com.gContactSync.LOGGER.VERBOSE_LOG(" - The group was renamed, but write-only mode was enabled");
803                   }
804                   else {
805                     com.gContactSync.LOGGER.LOG("  - The group's name changed, updating the list");
806                     list.setName(title);
807                     list.update();
808                   }
809                 }
810                 list.matched = true;
811               }
812               else { // the group is new
813                 if (ab.mPrefs.writeOnly == "true") {
814                   com.gContactSync.LOGGER.VERBOSE_LOG(" - The group is new, but write-only mode was enabled");
815                 }
816                 else {
817                   // make a new mailing list with the same name
818                   com.gContactSync.LOGGER.LOG("  - The group is new");
819                   var list = ab.addList(title, id);
820                   com.gContactSync.LOGGER.VERBOSE_LOG("  - List added to address book");
821                 }
822               }
823             }
824           }
825           catch (e) {
826             if (noCatch) throw e;
827             com.gContactSync.LOGGER.LOG_ERROR("Error while syncing groups: " + e);
828           }
829         }
830         com.gContactSync.LOGGER.LOG("***Looking for unmatched mailing lists***");
831         for (var i in com.gContactSync.Sync.mLists) {
832           var list = com.gContactSync.Sync.mLists[i];
833           if (list && !list.matched) {
834             // if it is new, make a new group in Google
835             if (i.indexOf("http://www.google.com/m8/feeds/groups/") == -1) {
836               com.gContactSync.LOGGER.LOG("-Found new list named " + list.getName());
837               com.gContactSync.LOGGER.VERBOSE_LOG(" * The URI is: " + list.getURI());
838               if (ab.mPrefs.readOnly == "true") {
839                 com.gContactSync.LOGGER.LOG(" * Ignoring since read-only mode is on");  
840               }
841               else {
842                 com.gContactSync.LOGGER.LOG(" * It will be added to Google");
843                 com.gContactSync.Sync.mGroupsToAdd.push(list);
844               }
845             }
846             // if it is old, delete it
847             else {
848                 com.gContactSync.LOGGER.LOG("-Found an old list named " + list.getName());
849                 com.gContactSync.LOGGER.VERBOSE_LOG(" * The URI is: " + list.getURI());
850                 if (ab.mPrefs.writeOnly == "true") {
851                   com.gContactSync.LOGGER.VERBOSE_LOG(" * Write-only mode was enabled so no action will be taken");
852                 }
853                 else {
854                   com.gContactSync.LOGGER.LOG(" * It will be deleted from Thunderbird");
855                   list.remove();
856                 }
857             }
858           }
859         }
860       }
861       else {
862         var groupName = ab.mPrefs.myContactsName.toLowerCase();
863         com.gContactSync.LOGGER.LOG("Only synchronizing the '" +
864                                     ab.mPrefs.myContactsName + "' group.");
865         var group, id, sysId, title;
866         var foundGroup = false;
867         for (var i = 0; i < arr.length; i++) {
868           try {
869             group = new com.gContactSync.Group(arr[i]);
870             // add the ID to mGroups by making a new property with the ID as the
871             // name and the title as the value for easy lookup for contacts
872             // Note: If someone wants to sync a group with the same name as a
873             // system group then this method won't work because system groups
874             // are first.
875             id    = group.getID();
876             sysId = group.getSystemId();
877             title = group.getTitle();
878             com.gContactSync.LOGGER.VERBOSE_LOG("  - Found a group named '"
879                                                 + title + "' with ID '"
880                                                 + id + "'");
881             title = title ? title.toLowerCase() : "";
882             sysId = sysId ? sysId.toLowerCase() : "";
883             if (sysId == groupName || title == groupName) {
884               foundGroup = true;
885               break;
886             }
887           }
888           catch (e) {com.gContactSync.alertError(e);}
889         }
890         if (foundGroup) {
891           com.gContactSync.LOGGER.LOG(" * Found the group to synchronize: " + id);
892           com.gContactSync.Sync.mContactsUrl = id;
893           return com.gContactSync.Sync.getContacts();
894         }
895         else {
896           var msg = " * Could not find the group '" + groupName + "' to synchronize."
897           com.gContactSync.LOGGER.LOG_ERROR(msg);
898           return com.gContactSync.Sync.syncNextUser();
899         }
900       }
901     }
902     com.gContactSync.LOGGER.LOG("***Deleting old groups from Google***");
903     return com.gContactSync.Sync.deleteGroups();
904   },
905   /**
906    * Deletes all of the groups in mGroupsToDelete one at a time to avoid timing
907    * issues.  Calls com.gContactSync.Sync.addGroups() when finished.
908    */
909   deleteGroups: function Sync_deleteGroups() {
910     var ab = com.gContactSync.Sync.mCurrentAb;
911     if (com.gContactSync.Sync.mGroupsToDelete.length == 0
912         || ab.mPrefs.readOnly == "true") {
913       com.gContactSync.LOGGER.LOG("***Adding new groups to Google***");
914       com.gContactSync.Sync.addGroups();
915       return;
916     }
917     var group = com.gContactSync.Sync.mGroupsToDelete.shift();
918     com.gContactSync.LOGGER.LOG("-Deleting group: " + group.getTitle());
919     var httpReq = new com.gContactSync.GHttpRequest("delete",
920                                                     com.gContactSync.Sync.mCurrentAuthToken,
921                                                     group.getEditURL(),
922                                                     null,
923                                                     com.gContactSync.Sync.mCurrentUsername);
924     httpReq.mOnSuccess = com.gContactSync.Sync.deleteGroups;
925     httpReq.mOnError   = function deleteGroupsError(httpReq) {
926       com.gContactSync.LOGGER.LOG_ERROR('Error while deleting group',
927                                         httpReq.responseText);
928       com.gContactSync.Sync.deleteGroups();
929     };
930     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
931     httpReq.addHeaderItem("If-Match", "*");
932     httpReq.send();
933   },
934   /**
935    * The first part of adding a group involves creating the XML representation
936    * of the mail list and then calling com.gContactSync.Sync.addGroups2() upon successful
937    * creation of a group.
938    */
939   addGroups: function Sync_addGroups() {
940     var ab = com.gContactSync.Sync.mCurrentAb;
941     if (com.gContactSync.Sync.mGroupsToAdd.length == 0
942         || ab.mPrefs.readOnly == "true") {
943       com.gContactSync.LOGGER.LOG("***Updating groups from Google***");
944       com.gContactSync.Sync.updateGroups();
945       return;
946     }
947     var list = com.gContactSync.Sync.mGroupsToAdd[0];
948     var group = new com.gContactSync.Group(null, list.getName());
949     com.gContactSync.LOGGER.LOG("-Adding group: " + group.getTitle());
950     var body = com.gContactSync.serialize(group.xml);
951     if (com.gContactSync.Preferences.mSyncPrefs.verboseLog.value)
952       com.gContactSync.LOGGER.VERBOSE_LOG(" * XML feed of new group:\n" + body);
953     var httpReq = new com.gContactSync.GHttpRequest("addGroup",
954                                                     com.gContactSync.Sync.mCurrentAuthToken,
955                                                     null,
956                                                     body,
957                                                     com.gContactSync.Sync.mCurrentUsername);
958     httpReq.mOnCreated = com.gContactSync.Sync.addGroups2;
959     httpReq.mOnError =   function addGroupError(httpReq) {
960       com.gContactSync.LOGGER.LOG_ERROR('Error while adding group',
961                                         httpReq.responseText);
962       com.gContactSync.Sync.mGroupsToAddURI.shift()
963       com.gContactSync.Sync.addGroups();
964     };
965     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
966     httpReq.send();
967   },
968   /**
969    * The second part of adding a group involves updating the list from which
970    * this group was created so the two can be matched during the next sync.
971    * @param aResponse {XMLHttpRequest} The HTTP request.
972    */
973   addGroups2: function Sync_addGroups2(aResponse) {
974     var group = new com.gContactSync.Group(aResponse.responseXML
975                                    .getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url,
976                                                            "entry")[0]);
977     if (com.gContactSync.Preferences.mSyncPrefs.verboseLog.value)
978       com.gContactSync.LOGGER.LOG(com.gContactSync.serializeFromText(aResponse.responseText));
979     var list = com.gContactSync.Sync.mGroupsToAdd.shift();
980     var id   = group.getID();
981     list.setNickName(id);
982     if (list.update)
983       list.update();
984     com.gContactSync.Sync.mLists[id] = list;
985     com.gContactSync.Sync.addGroups();
986   },
987   /**
988    * Updates all groups in mGroupsToUpdate one at a time to avoid timing issues
989    * and calls com.gContactSync.Sync.getContacts() when finished.
990    */
991   updateGroups: function Sync_updateGroups() {
992     var ab = com.gContactSync.Sync.mCurrentAb;
993     if (com.gContactSync.Sync.mGroupsToUpdate.length == 0
994         || ab.mPrefs.readOnly == "true") {
995       com.gContactSync.Sync.getContacts();
996       return;
997     }
998     var group = com.gContactSync.Sync.mGroupsToUpdate.shift();
999     com.gContactSync.LOGGER.LOG("-Updating group: " + group.getTitle());
1000     var body = com.gContactSync.serialize(group.xml);
1001     if (com.gContactSync.Preferences.mSyncPrefs.verboseLog.value)
1002       com.gContactSync.LOGGER.VERBOSE_LOG(" * XML feed of group: " + body);
1003     var httpReq = new com.gContactSync.GHttpRequest("update",
1004                                                     com.gContactSync.Sync.mCurrentAuthToken,
1005                                                     group.getEditURL(),
1006                                                     body,
1007                                                     com.gContactSync.Sync.mCurrentUsername);
1008     httpReq.mOnSuccess = com.gContactSync.Sync.updateGroups;
1009     httpReq.mOnError   = function updateGroupError(httpReq) {
1010       com.gContactSync.LOGGER.LOG_ERROR("Error while updating group",
1011                                         httpReq.responseText);
1012       com.gContactSync.Sync.updateGroups();
1013     };
1014     httpReq.mOnOffline = com.gContactSync.Sync.mOfflineFunction;
1015     httpReq.addHeaderItem("If-Match", "*");
1016     httpReq.send();
1017   },
1018   /**
1019    * Schedules another sync after the given delay if one is not already scheduled,
1020    * there isn't a sync currently running, if the delay is greater than 0, and
1021    * finally if the auto sync pref is set to true.
1022    * @param aDelay {integer} The duration of time to wait before synchronizing
1023    *                         again.
1024    */
1025   schedule: function Sync_schedule(aDelay) {
1026     // only schedule a sync if the delay is greater than 0, a sync is not
1027     // already scheduled, and autosyncing is enabled
1028     if (aDelay && !com.gContactSync.Preferences.mSyncPrefs.synchronizing.value &&
1029         !com.gContactSync.Sync.mSyncScheduled && aDelay > 0 &&
1030         com.gContactSync.Preferences.mSyncPrefs.autoSync.value) {
1031       com.gContactSync.Sync.mSyncScheduled = true;
1032       com.gContactSync.LOGGER.VERBOSE_LOG("Next sync in: " + aDelay + " milliseconds");
1033       setTimeout(com.gContactSync.Sync.begin, aDelay);  
1034     }
1035   }
1036 };
1037