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