// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;

CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockStringBundleHelpers.jsm");
CU.import("resource:///modules/FlockXMLUtils.jsm");
CU.import("resource:///modules/FlockWebmailUtils.jsm");
CU.import("resource:///modules/FlockPrefsUtils.jsm");

const MODULE_NAME = "AOLMail";
const CLASS_NAME = "Flock AOL Mail Service";
const CLASS_SHORT_NAME = "aolmail";
const CLASS_TITLE = "AOL Mail";
const CLASS_ID = Components.ID("{81690132-CA72-4041-AD8E-27FC6AA1337A}");
const CONTRACT_ID = "@flock.com/aolmail-service;1";

const API_CLASS_NAME = "AOLMail API";
const API_CLASS_ID = Components.ID("{22A698AF-46DD-44AB-93EB-4769189E5CB6}");
const API_CONTRACT_ID = "@flock.com/webservice/api/aolmail;1";

const SERVICE_ENABLED_PREF = "flock.service.aolmail.enabled";
const DISPLAY_UNREAD_COUNT_PREF = "flock.webmail.displayUnreadMessageCount";
const REFRESH_INTERVAL_PREF = "flock.webmail.refreshInterval";

const AOLMAIL_FAVICON = "chrome://flock/content/services/aolmail/favicon.png";
const AOLMAIL_STRING_BUNDLE = "chrome://flock/locale/services/aolmail.properties";

const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";

const XMLHTTPREQUEST_CONTRACTID = "@mozilla.org/xmlextras/xmlhttprequest;1";
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const STR_EMAIL = "email";
const STR_ISBASICVERSION = "isBasicVersion";
const STR_ISUTF16 = "isUTF16Encoded";
const STR_LANGUAGE = "language";
const STR_LOCALE = "locale";
const STR_UNREAD = "unreadMessages";
const STR_WAITING_TO_SEND = "waitingToSend";
const STR_API_TOKEN = "apiToken";
const STR_API_EXPIRES_ON = "apiExpiresOn";

const DOMAIN = "aol.com";
const MAX_UNREAD_COUNT = 75;  // max unread messages to retrieve
const MAX_DISPLAY_COUNT = 20;
const DEFAULT_DISPLAY_COUNT = 4;
const MORE_AUTHENTICATION_REQUIRED_CODE = 330;
const UNAUTHORIZED_AUTHENTICATION_REQUIRED_CODE = 401;

var gApi = null;


/**************************************************************************
 * Component: Flock AOLMailService
 **************************************************************************/

function AOLMailService() {
  this._init();

  var ws = FlockSvcUtils.flockIWebService;
  ws.addDefaultMethod(this, "getStringBundle");
  ws.addDefaultMethod(this, "isHidden");
  ws.addDefaultMethod(this, "url");

  var lws = FlockSvcUtils.flockILoginWebService;
  lws.addDefaultMethod(this, "loginURL");
  lws.addDefaultMethod(this, "getAccounts");
  lws.addDefaultMethod(this, "getAccount");
  lws.addDefaultMethod(this, "getAuthenticatedAccount");
  lws.addDefaultMethod(this, "getCredentialsFromForm");
  lws.addDefaultMethod(this, "getSessionValue");
  lws.addDefaultMethod(this, "logout");
  lws.addDefaultMethod(this, "ownsDocument");
  lws.addDefaultMethod(this, "ownsLoginForm");

  FlockSvcUtils.flockIRichContentDropHandler
               .addDefaultMethod(this, "_handleTextareaDrop");
}


/**************************************************************************
 * Flock AOLMailService: XPCOM Component Creation
 **************************************************************************/

AOLMailService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  AOLMailService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsIObserver,
    CI.nsITimerCallback,
    CI.flockIPollingService,
    CI.flockIWebService,
    CI.flockILoginWebService,
    CI.flockIMailWebService,
    CI.flockIRichContentDropHandler,
    CI.flockIMigratable
  ]
);

// FlockXPCOMUtils.genericModule() categories
AOLMailService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockMigratable" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }
];


/**************************************************************************
 * Flock AOLMailService: Private Data and Methods
 **************************************************************************/

// Service initialization
AOLMailService.prototype._init =
function amSvc__init() {
  FlockSvcUtils.getLogger(this).info(".init()");

  // Determine whether this service has been disabled, and unregister if so
  if (FlockPrefsUtils.getBoolPref(SERVICE_ENABLED_PREF) === false) {
    this._logger.info("Pref " + SERVICE_ENABLED_PREF
                              + " set to FALSE... not initializing.");
    this.deleteCategories();
    return;
  }

  gApi = CC[API_CONTRACT_ID].createInstance(CI.flockIAOLMailAPI);
  profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("aolmail-init");

  var obs = CC["@mozilla.org/observer-service;1"]
            .getService(CI.nsIObserverService);
  obs.addObserver(this, "xpcom-shutdown", false);

  var prefs = CC["@mozilla.org/preferences-service;1"]
              .getService(CI.nsIPrefBranch2);
  prefs.addObserver(REFRESH_INTERVAL_PREF, this, false);

  this._accountClassCtor = AOLMailAccount;
  FlockSvcUtils.getWD(this);
  FlockSvcUtils.getCoop(this);
  this._c_svc = this._coop.get(this.urn);
  if (!this._c_svc) {
    this._c_svc = new this._coop.Service(this.urn, {
      name: CLASS_SHORT_NAME,
      desc: CLASS_TITLE
    });
  }
  this._c_svc.serviceId = CONTRACT_ID;
  this._c_svc.logoutOption = true;
  this._c_svc.domains = this._WebDetective
                            .getString(CLASS_SHORT_NAME, "domains", DOMAIN);

  // Update auth states
  try {
    if (this._WebDetective
            .detectCookies(CLASS_SHORT_NAME, "loggedoutCookies", null))
    {
      this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    }
    gApi.deauthenticate();
  } catch (ex) {
    this._logger.error("ERROR updating auth states for "
                       + MODULE_NAME
                       + ": "
                       + ex);
  }

  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = AOLMAIL_STRING_BUNDLE;

  profiler.profileEventEnd(evtID, "");
}

// Migrate coop objects
AOLMailService.prototype._migrateFromCoop =
function amSvc_migrateFromCoop(aContext) {
  var accounts = this.getAccounts();
  while (accounts.hasMoreElements()) {
    var account = accounts.getNext();
    if (aContext.majorVersion < 2) {
      // 2.0 changes
      account.setCustomParam("isSessionExpired", false);
    }
  }
  // Makes this a generator-iterator
  yield true;
}

AOLMailService.prototype._refreshUnreadMessages =
function amSvc__refreshUnreadMessages(aAccount, aFlockListener) {
  this._logger.debug("_refreshUnreadMessages()");
  var svc = this;
  var newMailListener = {
    onSuccess: function rum_newMailListener_onSuccess(aResult, aTopic) {
      svc._logger.debug("newMailListener.onSuccess()");
      var messageList = aResult.wrappedJSObject.mailList.messageList;
      var getLink = function rum_getLink(aLink) {
        var link = aLink;
        // Return adjusted link if account is using basic version
        if (aAccount.getCustomParam(STR_ISBASICVERSION)) {
          // Get message id from link
          var uid_re = svc._WebDetective.getString(svc.shortName, "uid_re", "");
          var getId = link.match(uid_re);
          if (getId) {
            // Build basic link
            link = svc._WebDetective
                      .getString(svc.shortName, "basicMessageURL", "")
                      .replace("%id%", getId[1]);
          }
        }
        return link;
      };
      // Get the messages
      var newCount = 0;
      var msgCount = 0;
      var messages = [];
      var newestMessageDate = 0;
      for (var i = 0; i < messageList.length; i++) {
        var folder = messageList[i];
        if (folder.folderName.match(/inbox/gi)) {
          newCount = folder.totalCount;
          msgCount = folder.messageCount;
          for (var j = 0; j < msgCount; j++) {
            messages.push({
              from: folder.messages[j].sender,
              subject: folder.messages[j].subject,
              url: getLink(folder.messages[j].messageLink)
            });
            // Retrieve the date of the newest message
            if (!newestMessageDate) {
              var datetime_re = svc._WebDetective
                                   .getString(svc.shortName, "datetime_re", "");
              var sentOn = folder.messages[j].sentOn.match(datetime_re);
              if (sentOn) {
                // Parse the DateTime field.
                // Format example: 2008-02-26T19:32:19-05:00
                var year = sentOn[1];
                var month = parseInt(sentOn[2], 10) - 1;
                var date = parseInt(sentOn[3], 10);
                var hrs = parseInt(sentOn[4], 10);
                var min = parseInt(sentOn[5], 10);
                var sec = parseInt(sentOn[6], 10);
                var tzHrs = parseInt(sentOn[8], 10);
                var tzMin = parseInt(sentOn[9], 10);
                if (sentOn[7] == "-") {
                  hrs += tzHrs;
                  min += tzMin;
                } else {
                  hrs -= tzHrs;
                  min -= tzMin;
                }
                newestMessageDate = Date.UTC(year, month, date, hrs, min, sec);
              }
            }
          }
          break;
        }
      }

      var unreadMessages = {
        lastCheckedDate: Date.now(),
        newestMessageDate: newestMessageDate,
        unreadMessageCount: newCount,
        messages: messages
      };

      var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
      aFlockListener.onSuccess(nsJSON.encode(unreadMessages),
                               "_refreshUnreadMessages");
    },
    onError: function rum_newMailListener_onError(aFlockError, aTopic) {
      svc._logger.error("newMailListener.onError('" + aFlockError.errorCode + "')");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  var numDisplayed = FlockPrefsUtils.getIntPref(DISPLAY_UNREAD_COUNT_PREF);
  if (!numDisplayed || numDisplayed > MAX_DISPLAY_COUNT) {
    numDisplayed = DEFAULT_DISPLAY_COUNT;
  }

  gApi.getNewMailList(aAccount.getParam("accountId"),
                      "Inbox",
                      numDisplayed,
                      true,
                      aAccount.getCustomParam(STR_LANGUAGE),
                      aAccount.getCustomParam(STR_LOCALE),
                      aAccount,
                      newMailListener);
}


/**************************************************************************
 * Flock AOLMailService: flockIWebService Implementation
 **************************************************************************/

// readonly attribute AString title;
AOLMailService.prototype.title = CLASS_TITLE;

// readonly attribute AString shortName;
AOLMailService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute AString icon;
AOLMailService.prototype.icon = AOLMAIL_FAVICON;

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
// FIXME: File a bug for this as IDL is case-insensitive.
AOLMailService.prototype.contractId = CONTRACT_ID;

// readonly attribute AString urn;
AOLMailService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";

// DEFAULT: nsIStringBundle getStringBundle();
// DEFAULT: boolean isHidden();


/**************************************************************************
 * Flock AOLMailService: flockILoginWebService Implementation
 **************************************************************************/

// readonly attribute AString domains;
AOLMailService.prototype.__defineGetter__("domains",
function amSvc_getDomains() {
  this._logger.debug("Getting property: domains");
  return this._c_svc.domains;
});

// readonly attribute boolean needPassword;
AOLMailService.prototype.needPassword = true;

// addAccount(in AString aAccountID,
//            in boolean aIsTransient,
//            in flockIListener aFlockListener);
AOLMailService.prototype.addAccount =
function amSvc_addAccount(aAccountID, aIsTransient, aFlockListener) {
  this._logger.debug("addAccount('" + aAccountID + "', "
                                    + aIsTransient + ", aFlockListener)");

  if (!aAccountID) {
    if (aFlockListener) {
      var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
      // XXX See bug 10749 - flockIError cleanup
      error.errorCode = 9999;
      aFlockListener.onError(error, "addAccount");
    }
    return null;
  }

  var pw = this._acUtils.getPassword(this.urn + ":" + aAccountID);
  var user = pw ? pw.username : aAccountID;

  // Create a new account and add it to coop if it doesn't already exist
  var accountUrn =
    this._acUtils.createAccount(this, aAccountID, user, null, aIsTransient);

  // Get refresh interval from prefs and ensure it's within bounds
  FlockWebmailUtils.setRefreshInterval(this, accountUrn);

  // Add custom parameters to be used
  var params = CC["@mozilla.org/hash-property-bag;1"]
               .createInstance(CI.nsIWritablePropertyBag2);
  params.setPropertyAsAString(STR_EMAIL, "");
  params.setPropertyAsBool("isPrimary", false);
  params.setPropertyAsBool("isSessionExpired", false);
  params.setPropertyAsBool("hasNewMessages", false);
  params.setPropertyAsBool(STR_ISUTF16, false);
  params.setPropertyAsInt32("lastPrimaryDate", 0);
  params.setPropertyAsBool(STR_ISBASICVERSION, false);
  params.setPropertyAsAString(STR_LANGUAGE, "en");
  params.setPropertyAsAString(STR_LOCALE, "us");
  params.setPropertyAsAString(STR_UNREAD,
                              FlockWebmailUtils.createEmptyMessageAsJSON());
  params.setPropertyAsAString(STR_WAITING_TO_SEND, "");
  params.setPropertyAsAString(STR_API_TOKEN, "");
  params.setPropertyAsInt32(STR_API_EXPIRES_ON, 0);
  this._acUtils.addCustomParamsToAccount(params, accountUrn);

  // Instantiate account component
  var account = this.getAccount(accountUrn);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
}

// boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
AOLMailService.prototype.docRepresentsSuccessfulLogin =
function amSvc_docRepresentsSuccessfulLogin(aDocument) {
  this._logger.debug("docRepresentsSuccessfulLogin()");
  if (this._WebDetective.detect(this.shortName, "loggedin", aDocument, null)) {
    // We're logged in but can we get an account ID yet?
    if (this.getAccountIDFromDocument(aDocument)) {
      // Got the account ID
      return true;
    }
    // No account ID, but the page might be in flux due to scripts and ajax
    // events. Monitor the page a bit longer and see if the account ID shows up.
    // Note: we cannot call asyncDetect() here because we do not have access to
    // aDocument's container element.
    var svc = this;
    var timerCB = {
      tries: 0,
      notify: function am_drsl_notify(aTimer) {
        this.tries++;
        if (this.tries > 50) {
          // Didn't turn up
          aTimer.cancel();
        } else if (svc._WebDetective
                      .detect(svc.shortName, "accountinfo", aDocument, null))
        {
          // Success!
          aTimer.cancel();
          // Hack: Now that we know the page has loaded the info we need to
          // extract from it, we issue another "FlockDocumentReady" event. This
          // will be picked up by the observer in flockWebServiceManager, which
          // will in turn call this method again.
          var obs = CC["@mozilla.org/observer-service;1"]
                    .getService(CI.nsIObserverService);
          obs.notifyObservers(aDocument,
                              "FlockDocumentReady",
                              aDocument.location.href);
         }
      }
    };
    var asyncTimer = CC["@mozilla.org/timer;1"].createInstance(CI.nsITimer);
    asyncTimer.initWithCallback(timerCB, 100, CI.nsITimer.TYPE_REPEATING_SLACK);
  }
  return false;
}

// AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
AOLMailService.prototype.getAccountIDFromDocument =
function amSvc_getAccountIDFromDocument(aDocument) {
  this._logger.debug("getAccountIDFromDocument()");
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective
          .detect(this.shortName, "accountinfo", aDocument, results))
  {
    var matches = results.getPropertyAsAString("accountid")
                         .match(/\s*(\S+\w)\W*$/m);
    if (matches) {
      // Get the account name. Note: matches[1] will not be null.
      var accountName = matches[1];
      // Is it a full email address?
      var email = accountName.match(/(.+)@(.+)/);
      if (email) {
        // If the email address is from one of the AOL domains, strip off
        // everything after and including the "@".
        var domains = this.domains.split(",");
        for each (var domain in domains) {
          // Note: email[1] and email[2] will not be null.
          if (domain == email[2]) {
            // Found common domain so return account ID without "@domain". 
            return email[1];
          }
        }
      }
      // Not an email address or from an AOL domain, so use the full account
      // name as the ID.
      return accountName;
    }
  }
  return null;
}

// DEFAULT: nsISimpleEnumerator getAccounts();
// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: AString getSessionValue();
// DEFAULT: void logout();
// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

// void removeAccount(in AString aUrn);
AOLMailService.prototype.removeAccount =
function amSvc_removeAccount(aUrn) {
  this._logger.debug("removeAccount('" + aUrn + "')");
  this._acUtils.removeAccount(aUrn);
  FlockWebmailUtils.ensurePrimaryAccountExists();
}

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
//                                      in AString aAcctURN,
//                                      in flockIListener aFlockListener);
AOLMailService.prototype.updateAccountStatusFromDocument =
function amSvc_updateAccountStatusFromDocument(aDocument,
                                               aAcctURN,
                                               aFlockListener)
{
  this._logger.debug("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We are currently logged in to aAcctURN
    var account = this.getAccount(aAcctURN);
    if (!this._acUtils.ensureOnlyAuthenticatedAccount(aAcctURN)) {
      // Just logged in
      FlockWebmailUtils.onLogin(account);
      if (aFlockListener) {
        aFlockListener.onSuccess(account, null);
      }
    }
    // Pull information off of the webmail page
    var url = aDocument.location;
    // Get version
    var basicFlag = this._WebDetective.getString(this.shortName, "basic", "");
    var isBasic = (url.pathname.indexOf(basicFlag) > -1);
    account.setCustomParam(STR_ISBASICVERSION, isBasic);
    account.setCustomParam(STR_ISUTF16, isBasic);
    // Get language and locale
    var matched = url.pathname.match(/\/([a-z][a-z])-([a-z][a-z])\//i);
    if (matched) {
      account.setCustomParam(STR_LANGUAGE, matched[1]);
      account.setCustomParam(STR_LOCALE, matched[2]);
    }

    // Store the email address, if necessary
    account._storeEmail(aDocument);

    // Do we have a pending compose message?
    FlockWebmailUtils.checkForPendingComposeMessage(account);
  } else if (this._WebDetective
                 .detect(this.shortName, "loggedout", aDocument, null))
  {
    // Detected a logged out state
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    gApi.deauthenticate();

    // Ensure the webmail icon does not indicate new unread messages while
    // logged out of the primary account
    FlockWebmailUtils.updateIcon();
  }
}


/**************************************************************************
 * Flock AOLMailService: flockIPollingService Implementation
 **************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener aPollingListener);
AOLMailService.prototype.refresh =
function amSvc_refresh(aURN, aPollingListener) {
  this._logger.debug("refresh(" + aURN + ")");

  var account = this.getAccount(aURN);
  if (!account.isAuthenticated()) {
    this._coop.get(aURN).isPollable = false;
    aPollingListener.onResult();
    return;
  }
  var svc = this;
  var refreshListener = {
    onSuccess: function refresh_onSuccess(aSubject, aTopic) {
      svc._logger.debug("refresh_onSuccess: " + aURN + ": " + aSubject);
      FlockWebmailUtils.handleRefreshedMessages(svc, aURN, aSubject);
      aPollingListener.onResult();
    },
    onError: function refresh_onError(aFlockError, aTopic) {
      svc._logger.debug("unable to refresh unread messages for: " + aURN);
      aPollingListener.onError(aFlockError);
    }
  }
  this._refreshUnreadMessages(account, refreshListener);
}


/**************************************************************************
 * Flock AOLMailService: nsIObserver Implementation
 **************************************************************************/

// void observe(nsISupports subject, char* topic, PRUnichar* data);
AOLMailService.prototype.observe =
function amSvc_observe(aSubject, aTopic, aState) {
  switch (aTopic) {
    case "xpcom-shutdown":
      var obs = CC["@mozilla.org/observer-service;1"]
                .getService(CI.nsIObserverService);
      obs.removeObserver(this, "xpcom-shutdown");
      break;

    case "nsPref:changed":
      if (aState == REFRESH_INTERVAL_PREF) {
        this._logger.debug("Observer called: refresh interval changed\n");
        FlockWebmailUtils.setRefreshIntervalForAllAccounts(this);
      }
      break;
  }
}


/**************************************************************************
 * Flock AOLMailService: flockIRichContentDropHandler Implementation
 **************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours,
//                    in nsIDOMHTMLElement aTargetElement);
AOLMailService.prototype.handleDrop =
function amSvc_handleDrop(aFlavours, aTargetElement) {
  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    return FlockWebmailUtils.handleTextareaDrop(this,
                                                aFlavours,
                                                aTargetElement);
  }

  // Default handling otherwise
  return false;
};


/**************************************************************************
 * Flock AOLMailService: flockIMigratable Implementation
 **************************************************************************/

AOLMailService.prototype.__defineGetter__("migrationName",
function amSvc_getter_migrationName() {
  return flockGetString("common/migrationNames", "migration.name.aolmail");
});

// boolean needsMigration(in string aOldVersion);
AOLMailService.prototype.needsMigration =
function amSvc_needsMigration(aOldVersion) {
  // Version: major.minor.revision
  var major = parseInt(aOldVersion[0], 10);
  var minor = parseInt(aOldVersion[2], 10);
  if (major == 1 && minor == 2) {
    // Grouse: This service was added for Grouse and changed for Ibis.
    return true;
  }
  return false;
}

// nsISupports startMigration(in string aOldVersion,
//                            in flockIMigrationProgressListener aListener);
AOLMailService.prototype.startMigration =
function amSvc_startMigration(aOldVersion, aFlockMigrationProgressListener) {
  var ctxt = {
    majorVersion: parseInt(aOldVersion[0], 10),
    minorVersion: parseInt(aOldVersion[2], 10),
    listener: aFlockMigrationProgressListener
  };

  return { wrappedJSObject: ctxt };
}

// boolean doMigrationWork(in nsISupports aMigrationContext);
AOLMailService.prototype.doMigrationWork =
function amSvc_doMigrationWork(aMigrationContext) {
  var context = aMigrationContext.wrappedJSObject;
  if (!context.aolmailMigrator) {
    context.aolmailMigrator = this._migrateFromCoop(context);
  }
  if (context.aolmailMigrator.next()) {
    context.aolmailMigrator = null;
  }

  return (context.aolmailMigrator != null);
}

// void finishMigration(in nsISupports aMigrationContext);
AOLMailService.prototype.finishMigration =
function amSvc_finishMigration(aMigrationContext) {
}


/**************************************************************************
 * Component: Flock AOLMailAccount
 **************************************************************************/

// Constructor
function AOLMailAccount() {
  FlockSvcUtils.getLogger(this).init(CLASS_SHORT_NAME + ":Account");
  this._logger.info(".init()");
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  // TODO XXX: Should be able to use FlockSvcUtils.getWD() here, but it can
  //           only be called by the service.  Need to fix.
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "isAuthenticated");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "setParam");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "setCustomParam");

  // TODO XXX: Setting a private _shortName when adding the
  //           flockIWebmailAccount default methods should only be a
  //           temporary measure until the account has reference to its service
  this._shortName = CLASS_SHORT_NAME;
  var wma = FlockSvcUtils.flockIWebmailAccount;
  wma.addDefaultMethod(this, "getUnreadMessages");
  wma.addDefaultMethod(this, "getUpgradeAccount");
  wma.addDefaultMethod(this, "isPrimary");
  wma.addDefaultMethod(this, "isSessionExpired");
  wma.addDefaultMethod(this, "refreshUnreadMessageList");
  wma.addDefaultMethod(this, "setAsPrimary");
}


/**************************************************************************
 * Flock AOLMailAccount: XPCOM Component Creation
 **************************************************************************/

// Use genericComponent() for the goodness it provides (QI, nsIClassInfo, etc),
// but do NOT add AOLMailAccount to the list of constructors passed
// to FlockXPCOMUtils.genericModule().
AOLMailAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  AOLMailAccount,
  0,
  [
    CI.flockIWebServiceAccount,
    CI.flockIWebmailAccount
  ]
);


/**************************************************************************
 * Flock AOLMailAccount: flockIWebServiceAccount Implementation
 **************************************************************************/

// readonly attribute AString urn;
AOLMailAccount.prototype.urn = "";

// DEFAULT: flockILoginWebService getService();

// void login(in flockIListener aFlockListener);
AOLMailAccount.prototype.login =
function amAcct_login(aFlockListener) {
  this._logger.debug("login()");
  var inst = this;
  var authListener = {
    onSuccess: function login_authListener_onSuccess(aSubj, aTopic) {
      inst._logger.debug("login_authListeneronSuccess()");
      var c_acct = inst._coop.get(inst.urn);
      if (!c_acct.isAuthenticated) {
        c_acct.isAuthenticated = true;
        c_acct.isPollable = true;
      }
      if (aFlockListener) {
        aFlockListener.onSuccess(inst, "login");
      }
    },
    onError: function login_authListener_onError(aFlockError, aTopic) {
      inst._logger.error("login_authListener_onError()");
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, aTopic);
      }
    }
  };

  // Authenticate the API
  gApi._authenticate(this, null, authListener);
}

// void logout(in flockIListener aFlockListener);
AOLMailAccount.prototype.logout =
function amAcct_logout(aFlockListener) {
  this._logger.debug("logout()");
  var c_acct = this._coop.get(this.urn);
  if (c_acct.isAuthenticated) {
    c_acct.isAuthenticated = false;
    c_acct.isPollable = false;
    gApi.deauthenticate();
    FlockWebmailUtils.updateIcon();
    CC[c_acct.serviceId].getService(CI.flockIWebService).logout();
  }
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "logout");
  }
}

// void keep();
AOLMailAccount.prototype.keep =
function amAcct_keep() {
  this._logger.debug("keep()");
  this._coop.get(this.urn).isTransient = false;
  var urn = this.urn.replace("account:", "service:").replace("flock:", "");
  this._acUtils.makeTempPasswordPermanent(urn);

  // Automatically set the webmail account as the default mail client on the
  // first time the user configures a webmail account
  if (FlockPrefsUtils.getBoolPref("flock.webmail.promptAsDefaultMailer"))
  {
    FlockPrefsUtils.setBoolPref("flock.webmail.promptAsDefaultMailer", false);
    FlockPrefsUtils.setBoolPref("flock.webmail.useAsDefaultMailer", true);
  }
}

// DEFAULT: void isAuthenticated();
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);


/**************************************************************************
 * Flock AOLMailAccount: flockIWebmailAccount Implementation
 **************************************************************************/

// AString getComposeURL(in AString aMailParams);
AOLMailAccount.prototype.getComposeURL =
function amAcct_getComposeURL(aMailParams) {
  this._logger.debug("getComposeURL('" + aMailParams + "')");
  var version = this._getVersionString();
  var params = FlockWebmailUtils.reformatMailParams(this, aMailParams, ",");
  if (params && version == "standard") {
    // The "standard" AOL Mail dies if there are newlines and carriage returns
    // in the params, so we need to replace them. We use 0x08, framed by
    // spaces, as it is very unlikely to encounter this sequences in a mailto
    // string (and if we did, we'd want to replace it regardless). The spaces
    // ensure that the 0x08 is not "bound" to its neighbors when the composer
    // parses the content. These new values will get replaced when we call
    // onComposeURLLoaded() later on. 
    params = params.replace(/%0a/gi, "%20%08%20")
                   .replace(/%0d/gi, "%20%08%20");
  }

  // Get URL and add params
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME, version + "ComposeURL", "")
                .replace("%params%", params);
  this._logger.debug("compose URL is: " + url);
  return url;
}

// void onComposeURLLoaded(in AString aComposeURL,
//                         in nsIDOMXULElement aTabBrowser);
AOLMailAccount.prototype.onComposeURLLoaded =
function amAcct_onComposeURLLoaded(aComposeURL, aTabBrowser) {
  this._logger.debug("onComposeURLLoaded()");
  // There are two issues to deal with after the compose page has loaded:
  // 1. In "standard", we may have replaced linebreaks and carriage returns
  //    with "%20%08%20", so we have to look for those in the message body and
  //    replace with "<br >".
  // 2. In "basic", the body parameter needs to be added after the page loads.
  //    The standard "body=" param doesn't work, however, "richbody" seems to
  //    work for writing to the body of the rich text editor. I cannot find an
  //    equivalent for the plain text editor.
  if (this.getCustomParam(STR_ISBASICVERSION)) {
    this._onComposeURLLoadedBasic(aComposeURL, aTabBrowser);
  } else {
    this._onComposeURLLoadedStandard(aComposeURL, aTabBrowser);
  }
}

// AString getEmailAddress();
AOLMailAccount.prototype.getEmailAddress =
function amAcct_getEmailAddress() {
  this._logger.debug("getEmailAddress()");
  var email = this.getCustomParam(STR_EMAIL);
  if (!email) {
    email = this._coop.get(this.urn).accountId;
  }
  this._logger.debug("email address is: " + email);
  return email;
}

// AString getInboxURL();
AOLMailAccount.prototype.getInboxURL =
function amAcct_getInboxURL() {
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME,
                           this._getVersionString() + "InboxURL",
                           "");
  this._logger.debug("getInboxURL(): " + url);
  return url;
}

// DEFAULT: AString getUnreadMessages(in boolean aCheckNow);
// DEFAULT: AString getUpgradeAccount();
// DEFAULT: boolean isPrimary();
// DEFAULT: boolean isSessionExpired();
// DEFAULT: void refreshUnreadMessageList();
// DEFAULT: void setAsPrimary();


/**************************************************************************
 * Flock AOLMailAccount: Private Data and Methods
 **************************************************************************/

AOLMailAccount.prototype._onComposeURLLoadedBasic =
function amAcct__onComposeURLLoadedBasic(aComposeURL, aTabBrowser) {
  this._logger.debug("_onComposeURLLoadedBasic()");
  // We need to insert the message body into the message after the page has
  // loaded. Parse the mailto parameters out of the compose URL to get the body.
  var params = FlockWebmailUtils.parseMailParams(this, aComposeURL);
  if (!params.body) {
    // No body, so we're done
    return;
  }
  var inst = this;  // Wait for the text editor to load
  var results = FlockSvcUtils.newResults();
  var composerListener = {
    onSuccess: function bv_composeListener_onSuccess(aSubj, aTopic) {
      inst._logger.info("bv_composerListener: onSuccess()");

      // What loaded: Plain text or Rich text editor?
      var editorNode = results.getPropertyAsInterface("node", CI.nsIDOMNode);
      if (editorNode instanceof CI.nsIDOMHTMLTextAreaElement) {
        // Plain Text editor. We prepend the body data to the TEXTAREA data, in
        // case the user has a signature
        editorNode.value = params.body + editorNode.value;
        return;
      }

      // Rich Text editor
      if (editorNode instanceof CI.nsIDOMHTMLIFrameElement) {
        var bodyListener = {
          onSuccess: function bv_bodyListener_onSuccess(aSubj, aTopic) {
            inst._logger.info("bv_bodyListener: onSuccess()");
            var body = editorNode.contentDocument.body;
            // Replace the newlines with breaks. We prepend the body data to the
            // message body, in case the user has a signature
            body.innerHTML = params.body.replace(/\x0A/g, "<br />")
                           + body.innerHTML;
          },
          onError: function bv_bodyListener_onError(aFlockError, aTopic) {
            inst._logger.error("bv_bodyListener: onError()");
          }
        };
        // Wait for the body to get filled
        inst._WebDetective
            .asyncDetect(CLASS_SHORT_NAME, "basicRichComposerBody",
                         editorNode, null, bodyListener, 250, 80);
      }
    },
    onError: function bv_composerListener_onError(aFlockError, aTopic) {
      inst._logger.error("bv_composerListener: onError()");
    }
  };
  
  this._WebDetective
      .asyncDetect(CLASS_SHORT_NAME, "basicComposer",
                   aTabBrowser, results, composerListener, 250, 80);
}

AOLMailAccount.prototype._onComposeURLLoadedStandard =
function amAcct__onComposeURLLoadedStandard(aComposeURL, aTabBrowser) {
  this._logger.debug("_onComposeURLLoadedStandard()");
  // The Standard version cannot handle newlines in the compose URL so we need
  // to pound them into the message body after it has loaded, replacing the
  // "%20%08%20" sequences we inserted in getComposeURL() with "<br />".
  var inst = this;
  var results = FlockSvcUtils.newResults();
  var composerListener = {
    onSuccess: function nv_composeListener_onSuccess(aSubj, aTopic) {
      inst._logger.info("nv_composerListener: onSuccess()");

      // What loaded: Plain text or Rich text editor?
      var editorNode = results.getPropertyAsInterface("node", CI.nsIDOMNode);
      if (editorNode instanceof CI.nsIDOMHTMLTextAreaElement) {
        // Plain Text editor. Restore newlines.
        editorNode.value = editorNode.value.replace(/ ?\x08 ?/g, "\n");
        return;
      }

      // Rich Text editor
      if (editorNode instanceof CI.nsIDOMHTMLIFrameElement) {
        var bodyListener = {
          onSuccess: function bv_bodyListener_onSuccess(aSubj, aTopic) {
            inst._logger.info("bv_bodyListener: onSuccess()");
            var body = editorNode.contentDocument.body;
            // Replace the newlines with breaks
            body.innerHTML = body.innerHTML.replace(/ ?\x08 ?/g, "<br />");
          },
          onError: function bv_bodyListener_onError(aFlockError, aTopic) {
            inst._logger.error("bv_bodyListener: onError()");
          }
        };
        // Wait for the body to get filled
        inst._WebDetective
            .asyncDetect(CLASS_SHORT_NAME, "standardRichComposerBody",
                         editorNode, null, bodyListener, 250, 80);
      }
    },
    onError: function nv_composerListener_onError(aFlockError, aTopic) {
      inst._logger.error("nv_composerListener: onError()");
    }
  };
  // Wait for AOL Mail to load within the "ws_hat" shell
  this._WebDetective
      .asyncDetect(CLASS_SHORT_NAME, "standardComposer",
                   aTabBrowser, results, composerListener, 250, 80);
}

AOLMailAccount.prototype._getVersionString =
function amAcct__getVersionString() {
  return this.getCustomParam(STR_ISBASICVERSION) ? "basic" : "standard";
}

// Until AOL Mail supplies us with a proper API call, we will need to call this
// method to retrieve the email address; See bug 12445
AOLMailAccount.prototype._storeEmail =
function amAcct__storeEmail(aDocument) {
  // Email already stored, don't do it again
  if (this.getCustomParam(STR_EMAIL)) {
    return;
  }

  this._logger.debug("_storeEmail() called: no email stored");

  // Look for the email address in AOL's hidden tag
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective
          .detect(CLASS_SHORT_NAME, "emailaddress", aDocument, results))
  {
    var email = results.getPropertyAsAString("email");
    this.setCustomParam(STR_EMAIL, email);
    this._logger.debug("stored email from tag: " + email);
    return;
  }

  // We couldn't find the hidden tag, so we fall back on our "api" call.
  // Scrape the contents of the Basic Version Compose page as it is currently
  // the only way to retrieve the user's email address correctly
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME, "basicComposeURL", "")
                .replace("%params%", "");

  var req = CC[XMLHTTPREQUEST_CONTRACTID].createInstance(CI.nsIXMLHttpRequest);
  req.mozBackgroundRequest = true;
  req.open("GET", url, true);

  var inst = this;
  req.onreadystatechange = function aAcct__storeEmail_orsc(aEvent) {
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (req.status / 100 == 2) {
        // We only want to store the email if we can find it, otherwise ignore
        var re = inst._WebDetective.getString(CLASS_SHORT_NAME, "email", "");
        var email = req.responseText.match(new RegExp(re, "im"));
        if (email && email[1]) {
          inst.setCustomParam(STR_EMAIL, email[1]);
          inst._logger.debug("stored email from API: " + email[1]);
        }
      }
    }
  };

  req.send(null);
}


/**************************************************************************
 * Component: Flock AOLMailAPI
 **************************************************************************/

function AOLMailAPI() {
  this.shortName = CLASS_SHORT_NAME;
  FlockSvcUtils.getLogger(this);
  this._logger.init("AOLMailAPI");
  FlockSvcUtils.getWD(this);

  var wsa = FlockSvcUtils.flockIWebServiceAPI;
  wsa.addDefaultMethod(this, "getRequestMethod");
}

AOLMailAPI.prototype = new FlockXPCOMUtils.genericComponent(
  API_CLASS_NAME,
  API_CLASS_ID,
  API_CONTRACT_ID,
  AOLMailAPI,
  0,
  [
    CI.nsISupports,
    CI.nsIClassInfo,
    CI.flockIWebServiceAPI,
    CI.flockIAuthenticatedAPI,
    CI.flockITokenAPI,
    CI.flockIAOLMailAPI
  ]
);


/*************************************************************************
 * AOLMailAPI: flockIWebServiceAPI Implementation
 *************************************************************************/

/**
 * void call(in AString aApiMethod,
 *           in nsISupports aParams,
 *           in nsISupports aPostVars,
 *           in long aRequestMethod,
 *           in flockIListener aFlockListener);
 * @see flockIWebServiceAPI#call
 */
AOLMailAPI.prototype.call =
function AOLMailAPI_call(aApiMethod,
                         aParams,
                         aPostVars,
                         aRequestMethod,
                         aFlockListener)
{
  this._logger.debug(".call('" + aApiMethod + "', ...)");
  if (!aApiMethod || !aParams || !aFlockListener) {
    // The listener is required to report the results of the call
    this._logger.error("call: missing a parameter");
    return;
  }

  // Get user id and account from param list
  var params = aParams.wrappedJSObject;
  var userId = params.userId
  var account = params.account;
  // Remove them from param list
  params.userId = null;
  params.account = null;

  var api = this;
  var authListener = {
    onSuccess: function call_authListener_onSuccess(aSubj, aTopic) {
      api._logger.debug("call_authListener_onSuccess()");

      var requestListener = {
        onSuccess: function call_requestListener_onSuccess(aResp, aTopic) {
          api._logger.debug("call: requestListener.onSuccess()");
          aFlockListener.onSuccess(aResp, "call");
        },
        onError: function call_requestListener_onError(aFlockError, aTopic) {
          api._logger.error("call: requestListener.onError('" 
                            + aFlockError.errorCode + "')");
          // If aTopic is null then this is an error in the response and
          // not the request.
          if (!aTopic && api._isAuthError(aFlockError)) {
            // Additional authentication is required
            FlockWebmailUtils.handleAuthError(account);
          }
          aFlockListener.onError(aFlockError, aTopic);
        }
      };

      var mailURL = api._WebDetective.getString(api.shortName, "mailURL", "")
                                     .replace("%method%", aApiMethod);
      var body = api._WebDetective.getString(api.shortName, "mailBody", "")
                                  .replace("%token%", api.token);
      // Append passed-in parameters to the body
      for (var prop in params) {
        if (params[prop]) {
          // The body will always have the devId and token in it, so it's all
          // right to prepend each parameter with an ampersand.
          body += "&" + prop + "=" + params[prop];
        }
      }
      // Send the request
      api._sendRequest("GET", mailURL, body, requestListener);
    },
    onError: function call_authListener_onError(aFlockError, aTopic) {
      api._logger.error("call_authListener_onError()");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };
  // Ensure that API is authenticated
  this._authenticate(account, { username: userId }, authListener);
};

// flockIError getError(in AString aErrorCode);
AOLMailAPI.prototype.getError =
function amAPI_getError(aErrorCode) {
  this._logger.debug(".getError('" + aErrorCode +"')");
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  // Check for null as we want to handle an error code of 0 (lost connection)
  if (aErrorCode != null) {
    error.errorCode = aErrorCode;
  } else {
    error.errorCode = CI.flockIError.WEBMAIL_UNKNOWN_ERROR;
  }
  return error;
};

// flockIError getHttpError(in AString aHttpErrorCode);
AOLMailAPI.prototype.getHttpError =
function amAPI_getHttpError(aHttpErrorCode) {
  this._logger.debug(".getHttpError('" + aHttpErrorCode +"')");
  var error = CC[FLOCK_ERROR_CONTRACTID].createInstance(CI.flockIError);
  // Check for null as we want to handle an error code of 0 (lost connection)
  if (aHttpErrorCode != null) {
    error.errorCode = aHttpErrorCode;
  } else {
    error.errorCode = CI.flockIError.WEBMAIL_UNKNOWN_ERROR;
  }
  return error;
};


/*************************************************************************
 * AOLMailAPI: flockIAuthenticatedAPI Implementation
 *************************************************************************/

// readonly attribute boolean isAuthenticated;
AOLMailAPI.prototype.isAuthenticated = false;

AOLMailAPI.prototype.authenticate =
function amAPI_authenticate(aCredentials, aFlockListener) {
  this._logger.debug(".authenticate(" + (aCredentials
                                         ? "'" + aCredentials.username + "', "
                                         : "")
                                      + "...)");
  if (this.isAuthenticated && this.token && this.expiresOn > Date.now()) {
    if (aFlockListener) {
      aFlockListener.onSuccess(this, "authenticate");
    }
    return;
  }

  // Need to get a token
  var api = this;
  var requestListener = {
    onSuccess: function auth_requestListener_onSuccess(aResponse, aTopic) {
      api._logger.debug("authenticate: requestListener.onSuccess()");
      // Get the token information from the response
      api.token = aResponse.data.token.a;
      api.expiresOn = (aResponse.data.token.expiresIn * 1000) + Date.now();
      api.isAuthenticated = true;
      if (aFlockListener) {
        aFlockListener.onSuccess(api, "authenticate");
      }
    },
    onError: function auth_requestListener_onError(aFlockError, aTopic) {
      api._logger.error("authenticate: requestListener.onError('"
                        + aFlockError.errorCode + "')");
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, aTopic);
      }
    }
  };

  var authURL = this._WebDetective.getString(this.shortName, "authURL", "")
                                  .replace("%method%", "getToken");
  var body = this._WebDetective.getString(this.shortName, "authBody", "")
                               .replace("%userId%", aCredentials
                                                    ? aCredentials.username
                                                    : "");
  this._sendRequest("POST", authURL, body, requestListener);
};

// void deauthenticate();
AOLMailAPI.prototype.deauthenticate =
function amAPI_deauthenticate() {
  this._logger.debug(".deauthenticate()");
  this.isAuthenticated = false;
  this.token = null;
};


/*************************************************************************
 * AOLMailAPI: flockITokenAPI Implementation
 *************************************************************************/

// readonly attribute AString token;
AOLMailAPI.prototype.token = null;

// readonly attribute PRUint32 expiresOn;
AOLMailAPI.prototype.expiresOn = 0;


/*************************************************************************
 * AOLMailAPI: flockIAOLMailAPI Implementation
 *************************************************************************/

// void getNewMailCount(in AString aUid,
//                      in flockIWebmailAccount aAccount,
//                      in flockIListener aFlockListener);
AOLMailAPI.prototype.getNewMailCount =
function amAPI_getNewMailCount(aUid, aAccount, aFlockListener) {
  if (!aFlockListener) {
    // The listener is required to report the results of the call
    this._logger.error("getNewMailCount: missing aFlockListener parameter");
    return;
  }
  var params = {
    wrappedJSObject: { "userId": aUid, "account": aAccount }
  };

  var api = this;
  var callListener = {
    onSuccess: function gnmc_callListener_onSuccess(aResp, aTopic) {
      api._logger.debug("getNewMailCount: callListener.onSuccess()");
      var result = {
        wrappedJSObject: { "mailFolderCount": aResp.data.mailFolderCount }
      };
      aFlockListener.onSuccess(result, "getNewMailCount");
    },
    onError: function gnmc_callListener_onError(aFlockError, aTopic) {
      api._logger.error("getNewMailCount: callListener.onError('"
                        + aFlockError.errorCode + "')");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  this.call("newMailCount",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            callListener);
}

// void getNewMailList(in AString aUid,
//                     in AString aFolder,
//                     in AString aNumItems,
//                     in boolean aGetNewMailCount,
//                     in AString aLang,
//                     in AString aLocale,
//                     in flockIWebmailAccount aAccount,
//                     in flockIListener aFlockListener);
AOLMailAPI.prototype.getNewMailList =
function amAPI_getNewMailList(aUid,
                              aFolder,
                              aNumItems,
                              aGetNewMailCount,
                              aLang,
                              aLocale,
                              aAccount,
                              aFlockListener)
{
  if (!aFlockListener) {
    // The listener is required to report the results of the call
    this._logger.error("getNewMailList: missing aFlockListener parameter");
    return;
  }
  var params = {
    wrappedJSObject: { "userId": aUid,
                       "folder": aFolder,
                       "items": aNumItems,
                       "showCount": (aGetNewMailCount ? "yes" : null),
                       "lang": aLang,
                       "locale": aLocale,
                       "account": aAccount
                     }
  };

  var api = this;
  var callListener = {
    onSuccess: function gnml_callListener_onSuccess(aResp, aTopic) {
      api._logger.debug("getNewMailList: callListener.onSuccess()");
      var result = {
        wrappedJSObject: { "mailList": aResp.data.mailList }
      };
      aFlockListener.onSuccess(result, "getNewMailList");
    },
    onError: function gnml_callListener_onError(aFlockError, aTopic) {
      api._logger.error("getNewMailList: callListener.onError('"
                        + aFlockError.errorCode + "')");
      aFlockListener.onError(aFlockError, aTopic);
    }
  };

  this.call("newMailList",
            params,
            null,
            CI.flockIWebServiceAPI.GET,
            callListener);
}


/*************************************************************************
 * Flock AOLMailAPI: Private Data and Methods
 *************************************************************************/

// A wrapper around authenticate() which will pull in the token and expiration
// info from the given account.
AOLMailAPI.prototype._authenticate =
function amAPI__authenticate(aAccount, aCredentials, aFlockListener) {
  if (this.isAuthenticated && this.token && this.expiresOn > Date.now()) {
    if (aFlockListener) {
      aFlockListener.onSuccess(this, "_authenticate");
    }
    return;
  }

  // Grab the token information from the account if it is present
  if (aAccount) {
    this.token = aAccount.getCustomParam(STR_API_TOKEN);
    this.expiresOn = aAccount.getCustomParam(STR_API_EXPIRES_ON);
    // If we got valid info then mark the API as authenticated
    this.isAuthenticated = (this.token && this.expiresOn > Date.now());
  }
  // Get the user's login credentials if they were not supplied 
  var credentials;
  if (aCredentials) {
    credentials = aCredentials;
  } else if (aAccount) {
    var key = aAccount.urn.replace("account:", "service:")
                          .replace("flock:", "");
    credentials = aAccount._acUtils.getPassword(key);
  }
  var api = this;
  var authListener = {
    onSuccess: function a_authListener_onSuccess(aResp, aTopic) {
      api._logger.debug("_authenticate: authListener.onSuccess()");
      if (aAccount) {
        // Store the token info with the account
        aAccount.setCustomParam(STR_API_TOKEN, api.token);
        aAccount.setCustomParam(STR_API_EXPIRES_ON, api.expiresOn);
      }
      aFlockListener.onSuccess(api, "_authenticate");
    },
    onError: function a_authListener_onError(aFlockError, aTopic) {
      api._logger.error("_authenticate: authListener.onError('"
                        + aFlockError.errorCode + "')");
      if (api.token) {
        // This token may be stale. Clear it and try again to get a fresh token.
        api.token = "";
        api.expiresOn = 0;
        api.authenticate(credentials, this);
      } else {
        // If aTopic is null then this is an error in the response and
        // not the request.
        if (!aTopic && api._isAuthError(aFlockError)) {
          // Additional authentication is required
          FlockWebmailUtils.handleAuthError(aAccount);
        }
        aFlockListener.onError(aFlockError, aTopic);
      }
    }
  };
  this.authenticate(credentials, authListener);
}

// Detect API authentication errors
AOLMailAPI.prototype._isAuthError =
function amAPI__isAuthError(aFlockError) {
  return (aFlockError.errorCode == MORE_AUTHENTICATION_REQUIRED_CODE ||
          aFlockError.errorCode == UNAUTHORIZED_AUTHENTICATION_REQUIRED_CODE);
}

// Send a request to the AOL server and wait for a reply
AOLMailAPI.prototype._sendRequest =
function amAPI__sendRequest(aMethod, aURL, aBody, aFlockListener) {
  this._logger.debug("_sendRequest('" + aMethod + "', " +
                                  "'" + aURL + "', " +
                                  "'" + aBody + "')");

  if (aMethod != "POST" && aMethod != "GET") {
    aFlockListener.onError(this.getHttpError("405"), "_sendRequest");
    return;
  }

  var url = aURL;
  if (aMethod == "GET") {
    url += "?" + aBody;
  }

  var req = CC[XMLHTTPREQUEST_CONTRACTID]
            .createInstance(CI.nsIXMLHttpRequest)
            .QueryInterface(CI.nsIJSXMLHttpRequest);
  req.mozBackgroundRequest = true;
  req.open(aMethod, url, true);

  // "Referer" is misspelled on purpose here
  var referer = this._WebDetective.getString(this.shortName, "referer", "");
  req.setRequestHeader("Referer", referer);

  var body = null;
  if (aMethod == "POST") {
    req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    body = aBody;
  }

  var api = this;
  req.onreadystatechange = function aolapi_sr_orsc(aEvt) {
    if (req.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      var status = req.status;
      var responseText = req.responseText;
      api._logger.debug("._sendRequest() Status = " + status);
      api._logger.debug("._sendRequest() response = \n" + responseText);
      if (status / 100 == 2) {
        try {
          if (responseText) {
            var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
            var response = nsJSON.decode(responseText).response;
            if (response.statusCode == 200) {
              aFlockListener.onSuccess(response, "_sendRequest");
            } else {
              api._logger.debug("._sendRequest() response error - "
                                + response.statusCode);
              // Include a null topic in the onError call so the listener
              // knows that this is an error in the response and not in the
              // initial request.
              aFlockListener.onError(api.getHttpError(response.statusCode),
                                     null);
            }
          } else {
            api._logger.debug("._sendRequest - api returned no XML");
            var error = api.getError(CI.flockIError.WEBMAIL_API_INVALID_XML);
            aFlockListener.onError(error, "_sendRequest");
          }
        } catch (ex) {
          // Error parsing XML
          api._logger.error("._sendRequest() Parse error = " + ex);
          var error = api.getError(CI.flockIError.WEBMAIL_API_CANNOT_PARSE);
          aFlockListener.onError(error, "_sendRequest");
        }
      } else {
        // HTTP errors
        api._logger.debug("._sendRequest() HTTP Error - " + status);
        aFlockListener.onError(api.getHttpError(status), "_sendRequest");
      }
    }
  };

  req.send(body);
}


/**************************************************************************
 * XPCOM Support - Module Construction
 **************************************************************************/

// Create array of components.
var gComponentsArray = [AOLMailAPI, AOLMailService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      gComponentsArray);

/**************************************************************************
 * END XPCOM Support
 **************************************************************************/

