// 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://gre/modules/JSON.jsm");
CU.import("resource:///modules/webmailBackend.jsm");

const MODULE_NAME = "Gmail";
const CLASS_NAME = "Gmail Service";
const CLASS_SHORT_NAME = "gmail";
const CLASS_TITLE = "Gmail";
const CLASS_ID = Components.ID("{1e34e47c-ed9a-4148-9946-f8de40723542}");
const CONTRACT_ID = "@flock.com/gmail-service;1";

const SERVICE_ENABLED_PREF = "flock.service.gmail.enabled";
const AUTO_SET_PRIMARY_PREF = "flock.webmail.autoSetPrimaryAccount";
const DISPLAY_UNREAD_COUNT_PREF = "flock.webmail.displayUnreadMessageCount";

const GMAIL_URL = "http://mail.google.com";
const GMAIL_FAVICON = "chrome://flock/content/services/gmail/favicon.png";
const GMAIL_STRING_BUNDLE = "chrome://flock/locale/services/gmail.properties";

const FLOCK_ERROR_CONTRACTID = "@flock.com/error;1";
const PREFS = CC["@mozilla.org/preferences-service;1"]
              .getService(CI.nsIPrefBranch);

const STR_UNREAD = "unreadMessages";
const STR_UI_VERSION = "uiVersion";
const STR_WAITING_TO_SEND = "waitingToSend";

const DOMAIN = "google.com";
const MAX_UNREAD_COUNT = 20;  // Gmail max unread messages to retrieve
const REFRESH_INTERVAL = 600;  // 10 min in seconds

// Gmail Form ID diffs
const TO_ID = 5;
const CC_ID = 6;
const BCC_ID = 7;
const SUBJECT_ID = 8;
const ADD_CC_ID = 18;
const ADD_BCC_ID = 19;
const PLAIN_TEXT_EDITOR_ID = 44;
const RICH_TEXT_EDITOR_ID = 45;
const PLAIN_TEXT_BODY_ID = 48;
const RICH_TEXT_IFRAME_ID = -1;


/**************************************************************************
 * Component: Flock GmailService
 **************************************************************************/

// Constructor
function GmailService() {
  this._init();

  var ws = FlockSvcUtils.flockIWebService;
  ws.addDefaultMethod(this, "getAccount");
  ws.addDefaultMethod(this, "isHidden");
  ws.addDefaultMethod(this, "getAuthenticatedAccount");
  ws.addDefaultMethod(this, "getAccounts");
  ws.addDefaultMethod(this, "logout");
  ws.addDefaultMethod(this, "getStringBundle");

  var mws = FlockSvcUtils.flockIManageableWebService;
  mws.addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  mws.addDefaultMethod(this, "getAccountIDFromDocument");
  mws.addDefaultMethod(this, "getCredentialsFromForm");
  mws.addDefaultMethod(this, "ownsLoginForm");
  mws.addDefaultMethod(this, "getSessionValue");

  FlockSvcUtils.flockIMailWebService.addDefaultMethod(this, "getLoginURL");
}


/**************************************************************************
 * Flock GmailService: XPCOM Component Creation
 **************************************************************************/

GmailService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  GmailService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.nsIObserver,
    CI.nsITimerCallback,
    CI.flockIPollingService,
    CI.flockIWebService,
    CI.flockIManageableWebService,
    CI.flockIMailWebService
  ]
);

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


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

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

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

  profiler = CC["@flock.com/profiler;1"].getService(CI.flockIProfiler);
  var evtID = profiler.profileEventStart("gmail-init");

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

  this._accountClassCtor = GmailAccount;
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  if (this._coop.Service.exists(this.urn)) {
    this._c_svc = this._coop.get(this.urn);
  } else {
    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;
  // Note that using FlockSvcUtils.getWD here adds the "_WebDetective"
  // property to the service.
  this._c_svc.domains =
      FlockSvcUtils.getWD(this).getString(CLASS_SHORT_NAME, "domains", DOMAIN);
  this._c_svc.loginURL =
      FlockSvcUtils.getWD(this).getString(CLASS_SHORT_NAME,
                                          "userlogin",
                                          GMAIL_URL);

  this._authMgr = CC["@flock.com/authmgr;1"]
                  .getService(CI.flockIAuthenticationManager);
  this._authMgr.joinNetwork(CONTRACT_ID, "google");

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

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

  profiler.profileEventEnd(evtID, "");
  this.api = new GMailAPI();
}

GmailService.prototype._setAccountIsPollable =
function gmSvc__setAccountIsPollable(aAcctUrn, aIsPollable) {
  this._logger.debug("_setAccountIsPollable('" + aAcctUrn + "', "
                                               + aIsPollable + ")");
  var accountUrn = aAcctUrn;
  if (!accountUrn) {
    accountUrn = this._acUtils
                     .getFirstAuthenticatedAccountForService(CONTRACT_ID);
  }
  var c_acct = this._coop.get(accountUrn);
  if (c_acct) {
    c_acct.isPollable = aIsPollable;
  }
}


/**************************************************************************
 * Flock GmailService: flockIWebService Implementation
 **************************************************************************/

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

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

// readonly attribute AString icon;
GmailService.prototype.icon = GMAIL_FAVICON;

// readonly attribute AString url;
GmailService.prototype.url = GMAIL_URL;

// 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.
GmailService.prototype.contractId = CONTRACT_ID;

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

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

// flockIWebServiceAccount addAccount(in AString aAccountID,
//                                    in boolean aIsTransient,
//                                    in flockIListener aListener);
GmailService.prototype.addAccount =
function gmSvc_addAccount(aAccountID, aIsTransient, aListener) {
  this._logger.debug("addAccount('" + aAccountID + "', "
                                    + aIsTransient + ", aListener)");

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

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

  // Create a new account and add it to coop if it doesn't already exist
  var accountURN = "urn:flock:" + this.shortName + ":account:" + aAccountID;
  if (!this._coop.Account.exists(accountURN)) {
    var coopAccount = new this._coop.Account(accountURN, {
      accountId: aAccountID,
      favicon: this.icon,
      isPollable: true,
      isTransient: aIsTransient,
      name: user,
      refreshInterval: REFRESH_INTERVAL,
      service: this._c_svc,
      serviceId: this.contractId
    });
    this._coop.accounts_root.children.addOnce(coopAccount);

    // Add custom parameters to be used
    var params = CC["@mozilla.org/hash-property-bag;1"]
                 .createInstance(CI.nsIWritablePropertyBag2);
    params.setPropertyAsBool("isPrimary", false);
    params.setPropertyAsBool("hasNewMessages", false);
    params.setPropertyAsInt32("lastPrimaryDate", 0);
    params.setPropertyAsAString(STR_UNREAD,
                                webmailBackend.createEmptyMessageAsJSON());
    params.setPropertyAsAString(STR_UI_VERSION, "1");
    params.setPropertyAsAString(STR_WAITING_TO_SEND, "");
    params.setPropertyAsBool("isUTF16Encoded", false);
    this._acUtils.addCustomParamsToAccount(params, accountURN);
  }

  account = this.getAccount(accountURN);

  if (aListener) {
    aListener.onSuccess(account, "addAccount");
  }

  return account;
}

// void removeAccount(in AString aUrn);
GmailService.prototype.removeAccount =
function gmSvc_removeAccount(aUrn) {
  this._logger.debug("removeAccount('" + aUrn + "')");
  this._acUtils.removeAccount(aUrn);
  webmailBackend.ensurePrimaryAccountExists();
}

// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: nsISimpleEnumerator getAccounts();
// DEFAULT: void logout();

// readonly attribute long status;
GmailService.prototype.status = CI.flockIWebService.STATUS_UNKNOWN;


/**************************************************************************
 * Flock GmailService: flockIManageableWebService Implementation
 **************************************************************************/

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

// readonly attribute AString loginURL;
GmailService.prototype.__defineGetter__("loginURL",
function gmSvc_getLoginURL() {
  this._logger.debug("Getting property: loginURL");
  return this._c_svc.loginURL;
});

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsIPassword getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
GmailService.prototype.ownsDocument =
function gmSvc_ownsDocument(aDocument)
{
  this._logger.debug("domain: " + aDocument.domain);

  // We need to override the default ownsDocument in this case because
  // multiple services share the same login page (google.com in this case)
  return this._WebDetective.detect(this.shortName,
                                   "ownsDocument",
                                   aDocument,
                                   null);
}

// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);

// void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
//                                      in AString aAcctURN);
GmailService.prototype.updateAccountStatusFromDocument =
function gmSvc_updateAccountStatusFromDocument(aDocument, aAcctURN) {
  this._logger.debug("updateAccountStatusFromDocument('" + aAcctURN + "')");
  if (aAcctURN) {
    // We are currently logged in to aAcctURN
    this._authMgr.reportCompleteLoginToNetwork(aAcctURN);
    if (!this._acUtils.ensureOnlyAuthenticatedAccount(aAcctURN)) {
      // Just logged in
      webmailBackend.onLogin(this.getAccount(aAcctURN));
    }

    // Store the UI Version: default to the first version
    var uiVersion = "1";
    if (this._WebDetective.detect(this.shortName,
                                  "uiVersion2",
                                  aDocument,
                                  null))
    {
      uiVersion = "2";
    }
    this.getAccount(aAcctURN).setCustomParam(STR_UI_VERSION, uiVersion);

    // Do we have a pending compose message?
    webmailBackend.checkForPendingComposeMessage(this.getAccount(aAcctURN));
  } else if (this._WebDetective
                 .detect(this.shortName, "loggedout", aDocument, null))
  {
    // Ensure the poller ignores the account we are about to log out of
    this._setAccountIsPollable(null, false);

    // Detected a logged out state
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    this._authMgr.reportCompleteLogoutFromNetwork(this.contractId);

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


/**************************************************************************
 * Flock GmailService: flockIMailWebService Implementation
 **************************************************************************/

// DEFAULT: AString getLoginURL();


/**************************************************************************
 * Flock GmailService: flockIPollingService Implementation
 **************************************************************************/

// void refresh(in AString aUrn, in flockIPollingListener aListener);
GmailService.prototype.refresh =
function gmSvc_refresh(aURN, aPollingListener) {
  this._logger.debug("GmailAccount refresh('" + aURN + "')");

  var account = this.getAccount(aURN);
  if (!account.isAuthenticated()) {
    return;
  }
  var username = account.getParam("name");
  var inst = this;
  var listener = {
    onSuccess: function gmSvc_refreshSuccess(aData) {
      if (aData) {
        webmailBackend.handleRefreshedMessages(inst, aURN, aData);
        aPollingListener.onResult();
      } else {
        aPollingListener.onError(null);
      }
    },
    onError: function  gmSvc_refreshError() {
      inst._logger.error("refresh_onError()");
      aPollingListener.onError(null);
    }
  };
  this.api.getUnreadMessages(username, listener);
}


/**************************************************************************
 * Flock GmailService: nsIObserver Implementation
 **************************************************************************/

GmailService.prototype.observe =
function gmSvc_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;
  }
}


/**************************************************************************
 * Component: Flock GmailAccount
 **************************************************************************/

// Constructor
function GmailAccount() {
  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, "deactivate");
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "login");
  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
  //           flockIMailWebServiceAccount default methods should only be a
  //           temporary measure until the account has reference to its service
  this._shortName = CLASS_SHORT_NAME;
  var mwsa = FlockSvcUtils.flockIMailWebServiceAccount;
  mwsa.addDefaultMethod(this, "getInboxURL");
  mwsa.addDefaultMethod(this, "getUnreadMessages");
  mwsa.addDefaultMethod(this, "getUpgradeAccount");
  mwsa.addDefaultMethod(this, "isPrimary");
  mwsa.addDefaultMethod(this, "refreshUnreadMessageList");
  mwsa.addDefaultMethod(this, "setAsPrimary");
}


/**************************************************************************
 * Flock GmailAccount: XPCOM Component Creation
 **************************************************************************/

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

/**************************************************************************
 * Flock GmailAccount: flockIWebServiceAccount Implementation
 **************************************************************************/

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

// void activate(in flockIListener aListener);
GmailAccount.prototype.activate =
function gmAcct_activate(aListener) {
  this._logger.debug("activate()");
  this._coop.get(this.urn).isAuthenticated = true;
  webmailBackend.onLogin(this);

  if (aListener) {
    aListener.onSuccess(this, "activate");
  }
}

// DEFAULT: void deactivate(in flockIListener aListener);
// DEFAULT: void login(in flockIListener aListener);

// void logout(in flockIListener aListener);
GmailAccount.prototype.logout =
function gmAcct_logout(aListener) {
  this._logger.debug("logout()");
  var c_acct = this._coop.get(this.urn);
  if (c_acct.isAuthenticated) {
    c_acct.isAuthenticated = false;
    webmailBackend.updateIcon();
    CC[c_acct.serviceId].getService(CI.flockIWebService).logout();
  }
  if (aListener) {
    aListener.onSuccess(this, "logout");
  }
}

// void keep();
GmailAccount.prototype.keep =
function gmAcct_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 (PREFS.getPrefType("flock.webmail.promptAsDefaultMailer") &&
      PREFS.getBoolPref("flock.webmail.promptAsDefaultMailer"))
  {
    PREFS.setBoolPref("flock.webmail.promptAsDefaultMailer", false);
    PREFS.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 GmailAccount: flockIMailWebServiceAccount Implementation
 **************************************************************************/

// AString getComposeURL(in AString aMailParams);
GmailAccount.prototype.getComposeURL =
function gmAcct_getComposeURL(aMailParams) {
  this._logger.debug("getComposeURL('" + aMailParams + "')");
  var params = webmailBackend.reformatMailParams(this, aMailParams, ",");
  var url = this._WebDetective
                .getString(CLASS_SHORT_NAME,
                           "composeURL_v" + this.getCustomParam(STR_UI_VERSION),
                           "")
                .replace("%params%", params);
  this._logger.debug("compose URL is: " + url);
  return url;
}

// AString getEmailAddress();
GmailAccount.prototype.getEmailAddress =
function gmAcct_getEmailAddress() {
  this._logger.debug("getEmailAddress()");
  var email = this._coop.get(this.urn).accountId;
  this._logger.debug("email address is: " + email);
  return email;
}

// void onComposeURLLoaded(in AString aComposeURL,
//                         in nsIDOMXULElement aTabBrowser);
GmailAccount.prototype.onComposeURLLoaded =
function gmAcct_onComposeURLLoaded(aComposeURL, aTabBrowser) {
  this._logger.debug("onComposeURLLoaded()");
  // Do post-processing depending on which version of the UI is loaded
  switch (this.getCustomParam(STR_UI_VERSION)) {
    case "2":
      this._onComposeURLLoadedVersion2(aComposeURL, aTabBrowser);
      break;
    case "1":
    default:
      this._onComposeURLLoadedVersion1(aComposeURL, aTabBrowser);
      break;
  }
}

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

GmailAccount.prototype._onComposeURLLoadedVersion1 =
function gmAcct__onComposeURLLoadedVersion1(aComposeURL, aTabBrowser) {
  this._logger.debug("_onComposeURLLoadedVersion1()");
  // Gmail has an older version of its composer, in which newlines are accepted
  // but not displayed. This is the default composer for some locales until they
  // are brought up to date. In this case, we need to replace the newlines in
  // the message with "<br />". To get to the message text, we need to drill
  // down through a frame and two iframes to get to the composer's document.
  var inst = this;
  // Make sure the frame has been loaded onto the page
  var frameListener = {
    onSuccess: function fListener_onSuccess(aSubj, aTopic) {
      inst._logger.info("frameListener: onSuccess()");
      // The frame doesn't have an id so we have to look for it by name.
      // Unfortunately, this name isn't unique so we have to check each element
      // it finds.
      var frame = null;
      var frameName = inst._WebDetective
                          .getString(CLASS_SHORT_NAME,
                                     "composerFrameName_v1",
                                     "main");
      var elements = aTabBrowser.contentDocument.getElementsByName(frameName);
      for (var i = 0; i < elements.length; i++) {
        if (elements[i] instanceof CI.nsIDOMHTMLFrameElement) {
          frame = elements[i];
          break;
        }
      }
      if (frame) {
        // Wait for frame's contents to load
        var iFrame1Listener = {
          onSuccess: function if1Listener_onSuccess(aSubj, aTopic) {
            inst._logger.info("iFrame1Listener: onSuccess()");
            // Get the frame's iFrame
            var iFrameId = inst._WebDetective
                               .getString(CLASS_SHORT_NAME,
                                          "composerIFrame1Id_v1",
                                          "v1");
            var iFrame1 = frame.contentDocument.getElementById(iFrameId);
            if (iFrame1 &&
                iFrame1 instanceof CI.nsIDOMHTMLIFrameElement)
            {
              // Get the iFrame's content
              var iFrame2Listener = {
                onSuccess: function if2Listener_onSuccess(aSubj, aTopic) {
                  inst._logger.info("iFrame2Listener: onSuccess()");
                  // Get the actual composer's iFrame
                  iFrameId = inst._WebDetective
                                 .getString(CLASS_SHORT_NAME,
                                            "composerIFrame2Id_v1",
                                            "hc_compose");
                  var iFrame2 = iFrame1.contentDocument
                                       .getElementById(iFrameId);
                  if (iFrame2 &&
                      iFrame2 instanceof CI.nsIDOMHTMLIFrameElement)
                  {
                    // Get the message content
                    var bodyListener = {
                      onSuccess: function bListener_onSuccess(aSubj, aTopic) {
                        inst._logger.info("bodyListener: onSuccess()");
                        // Finally! Replace newlines with breaks
                        var body = iFrame2.contentDocument.body;
                        body.innerHTML = body.innerHTML.replace(/\x0A/g,
                                                                "<br />");
                        // Ensure the page is editable (See bug 12145)
                        iFrame2.contentDocument.designMode = "on";
                      },
                      onError: function bListener_onError(aSubj, aTopic, aErr) {
                        inst._logger.error("bodyListener: onError()");
                        // Ensure the page is editable (See bug 12145)
                        iFrame2.contentDocument.designMode = "on";
                      }
                    };
                    // Wait for the message body to get filled
                    inst._WebDetective
                        .asyncDetect(CLASS_SHORT_NAME, "composerBody_v1",
                                     iFrame2, null, bodyListener, 200, 50);
                  }
                },
                onError: function if2Listener_onError(aSubj, aTopic, aErr) {
                  inst._logger.error("iFrame2Listener: onError()");
                }
              };
              // Wait for iFrame1 to get filled
              inst._WebDetective
                  .asyncDetect(CLASS_SHORT_NAME, "composerIFrame2_v1",
                               iFrame1, null, iFrame2Listener, 200, 50);
            }
          },
          onError: function if1Listener_onError(aSubj, aTopic, aErr) {
            inst._logger.error("iFrame1Listener: onError()");
          }
        };
        // Wait for the frame to get filled
        inst._WebDetective
            .asyncDetect(CLASS_SHORT_NAME, "composerIFrame1_v1",
                         frame, null, iFrame1Listener, 200, 50 );
      }
    },
    onError: function fListener_onError(aSubj, aTopic, aErr) {
      inst._logger.error("frameListener: onError()");
    }
  };
  // Wait for the main frame to show up
  inst._WebDetective
      .asyncDetect(CLASS_SHORT_NAME, "composerFrame_v1",
                   aTabBrowser, null, frameListener, 250, 80);
}

GmailAccount.prototype._onComposeURLLoadedVersion2 =
function gmAcct__onComposeURLLoadedVersion2(aComposeURL, aTabBrowser) {
  this._logger.debug("_onComposeURLLoadedVersion2()");
  // Gmail's "newer" composer used to accept mailto parameters properly but this
  // functionality stopped on January 10, 2008. This function will plug in data
  // to all of the message fields after the page has loaded. If the mailto
  // functionality is restored, this post-processing can be turned off by
  // changing the "onComposeURLLoadedVersion2" string in gmail.xml to "none".
  var action = this._WebDetective.getString(CLASS_SHORT_NAME,
                                            "onComposeURLLoadedVersion2",
                                            "none");
  if (action == "none") {
    return;
  }

  var inst = this;
  // Make sure the iFrame has been loaded onto the page
  var iFrame1Listener = {
    onSuccess: function if1Listener_onSuccess(aSubj, aTopic) {
      inst._logger.info("iFrame1Listener: onSuccess()");
      // Get the top iframe
      var iFrameId = inst._WebDetective
                         .getString(CLASS_SHORT_NAME,
                                    "composerIFrame1Id_v2",
                                    "canvas_frame");
      var iFrame1 = aTabBrowser.contentDocument.getElementById(iFrameId);
      if (iFrame1 && iFrame1 instanceof CI.nsIDOMHTMLIFrameElement) {
        // Wait for iframe's form to load
        var formResults = CC["@mozilla.org/hash-property-bag;1"]
                          .createInstance(CI.nsIWritablePropertyBag2);
        var formListener = {
          onSuccess: function formListener_onSuccess(aSubj, aTopic) {
            inst._logger.info("formListener: onSuccess()");
            // Parse the mailto parameters and values out of the compose URL
            var paramList = webmailBackend.parseMailParams(inst, aComposeURL);
            // Get the form Id
            var formId = formResults.getPropertyAsAString("formId");
            // The form elements have dynamic ids, however we know they are
            // base-36 numerals, and we know the relative difference between
            // them, so we can generate the ids from the form id.
            var formIdValue = parseInt(formId, 36);
            function _getFormElementID(aDiff) {
              return (formIdValue - aDiff).toString(36);
            }
            var frameDoc = iFrame1.contentDocument;
            // To
            if (paramList.to) {
              var toId = _getFormElementID(TO_ID);
              var toElement = frameDoc.getElementById(toId);
              if (toElement &&
                  toElement instanceof CI.nsIDOMHTMLTextAreaElement)
              {
                toElement.value = paramList.to;
              }
            }
            // Cc
            if (paramList.cc) {
              var ccId = _getFormElementID(CC_ID);
              var ccElement = frameDoc.getElementById(ccId);
              if (ccElement &&
                  ccElement instanceof CI.nsIDOMHTMLTextAreaElement)
              {
                ccElement.value = paramList.cc;
                // Show the Cc field and hide the "Add Cc" link
                var addCcId = _getFormElementID(ADD_CC_ID);
                var addCcElement = frameDoc.getElementById(addCcId);
                if (addCcElement &&
                    addCcElement.nodeName.toLowerCase() == "span")
                {
                  // We're not able to remove or hide the element, so we have to
                  // simulate a click on the link
                  var win = CC["@mozilla.org/appshell/window-mediator;1"]
                            .getService(CI.nsIWindowMediator)
                            .getMostRecentWindow("navigator:browser");
                  var evt = frameDoc.createEvent("MouseEvents");
                  evt.initMouseEvent("click", true, true, win, 0, 0, 0, 0, 0,
                                     false, false, false, false, 0, null);
                  addCcElement.dispatchEvent(evt);
                }
              }
            }
            // Bcc
            if (paramList.bcc) {
              var bccId = _getFormElementID(BCC_ID);
              var bccElement = frameDoc.getElementById(bccId);
              if (bccElement &&
                  bccElement instanceof CI.nsIDOMHTMLTextAreaElement)
              {
                bccElement.value = paramList.bcc;
                // Show the Bcc field and hide the "Add Bcc" link
                var addBccId = _getFormElementID(ADD_BCC_ID);
                var addBccElement = frameDoc.getElementById(addBccId);
                if (addBccElement &&
                    addBccElement.nodeName.toLowerCase() == "span")
                {
                  // We're not able to remove or hide the element, so we have to
                  // simulate a click on the link
                  var win = CC["@mozilla.org/appshell/window-mediator;1"]
                            .getService(CI.nsIWindowMediator)
                            .getMostRecentWindow("navigator:browser");
                  var evt = frameDoc.createEvent("MouseEvents");
                  evt.initMouseEvent("click", true, true, win, 0, 0, 0, 0, 0,
                                     false, false, false, false, 0, null);
                  addBccElement.dispatchEvent(evt);
                }
              }
            }
            // Subject
            if (paramList.subject) {
              var subjId = _getFormElementID(SUBJECT_ID);
              var subjElement = frameDoc.getElementById(subjId);
              if (subjElement &&
                  subjElement instanceof CI.nsIDOMHTMLInputElement)
              {
                subjElement.value = paramList.subject;
              }
            }
            // Body
            if (paramList.body) {
              // We need to wait for the preferred editor to be shown
              var plainTextId = _getFormElementID(PLAIN_TEXT_EDITOR_ID);
              var richTextId = _getFormElementID(RICH_TEXT_EDITOR_ID);
              var timerCB = {
                _tries: 0,
                notify: function ocul_v2_notify(aTimer) {
                  this._tries++;
                  inst._logger.debug("ocul_v2_notify: tries = " + this._tries);
                  if (this._tries > 25) {
                    inst._logger.error("ocul_v2_notify: timeout");
                    aTimer.cancel();
                    return;
                  }
                  var plainTextDiv = frameDoc.getElementById(plainTextId);
                  var richTextDiv = frameDoc.getElementById(richTextId);
                  // Is the plain text editor being shown?
                  if (plainTextDiv &&
                      plainTextDiv instanceof CI.nsIDOMHTMLDivElement &&
                      plainTextDiv.style.display != "none")
                  {
                    // Plain text
                    inst._logger.info("ocul_v2_notify: plain text");
                    aTimer.cancel();
                    var bodyId = _getFormElementID(PLAIN_TEXT_BODY_ID);
                    var bodyElement = frameDoc.getElementById(bodyId);
                    if (bodyElement &&
                        bodyElement instanceof CI.nsIDOMHTMLTextAreaElement)
                    {
                      // We prepend the body data to the TEXTAREA data, in case
                      // the user has a signature
                      bodyElement.value = paramList.body + bodyElement.value;
                      // Set the focus to clear the drop-down that occurs when
                      // the "to" field is filled.
                      bodyElement.focus();
                    }
                  } else if (richTextDiv &&
                             richTextDiv instanceof CI.nsIDOMHTMLDivElement &&
                             richTextDiv.style.display != "none")
                  {
                    // Rich text
                    inst._logger.info("ocul_v2_notify: rich text");
                    aTimer.cancel();
                    // Make sure IFrame exists (this element is a DIV when in
                    // plain text mode, so we need to make sure it is an iframe
                    // before moving on 
                    var iFrame2Listener = {
                      onSuccess: function if2Listener_onSuccess(aSubj, aTopic)
                      {
                        inst._logger.info("iFrame2Listener: onSuccess()");
                        // Get the iFrame
                        iFrameId = _getFormElementID(RICH_TEXT_IFRAME_ID);
                        var iFrame2 = frameDoc.getElementById(iFrameId);
                        if (iFrame2 &&
                            iFrame2 instanceof CI.nsIDOMHTMLIFrameElement)
                        {
                          // Get the message content
                          var bodyListener = {
                            onSuccess: function bListener_onSuccess(aSubj,
                                                                    aTopic)
                            {
                              inst._logger.info("bodyListener: onSuccess()");
                              var body = iFrame2.contentDocument.body;
                              // We prepend the body data to the message body,
                              // in case the user has a signature
                              body.innerHTML = paramList.body.replace(/\x0A/g,
                                                                      "<br />")
                                             + body.innerHTML;
                              // Ensure the page is editable (See bug 12145)
                              iFrame2.contentDocument.designMode = "on";
                              // Set the focus to clear the drop-down that
                              // occurs when the "to" field is filled
                              body.focus();
                            },
                            onError: function bListener_onError(aSubj, aTopic,
                                                                aErr)
                            {
                              inst._logger.error("bodyListener: onError()");
                              // Ensure the page is editable (See bug 12145)
                              iFrame2.contentDocument.designMode = "on";
                            }
                          };
                          // Wait for the iframe to get filled
                          inst._WebDetective
                              .asyncDetect(CLASS_SHORT_NAME,
                                           "composerBody_v2", iFrame2, null,
                                           bodyListener, 200, 50);
                        }
                      },
                      onError: function if2Listener_onError(aSubj, aTopic,
                                                               aErr)
                      {
                        inst._logger.error("iFrame2Listener: onError()");
                      }
                    };
                    // Wait for the iframe to get loaded
                    inst._WebDetective
                        .asyncDetect(CLASS_SHORT_NAME, "composerIFrame2_v2",
                                     iFrame1, null, iFrame2Listener, 250, 40);              
                  }
                }
              };
              var asyncTimer = CC["@mozilla.org/timer;1"]
                               .createInstance(CI.nsITimer);
              asyncTimer.initWithCallback(timerCB, 200,
                                          CI.nsITimer.TYPE_REPEATING_SLACK);
            }
          },
          onError: function formListener_onError(aSubj, aTopic, aErr) {
            inst._logger.error("formListener: onError()");
          }
        };
        // Wait for the iframe to get filled
        inst._WebDetective
            .asyncDetect(CLASS_SHORT_NAME, "composerForm_v2",
                         iFrame1, formResults, formListener, 250, 50);
      }
    },
    onError: function if1Listener_onError(aSubj, aTopic, aErr) {
      inst._logger.error("iFrame1Listener: onError()");
    }
  };
  // Wait for the iFrame to get loaded
  inst._WebDetective
      .asyncDetect(CLASS_SHORT_NAME, "composerIFrame1_v2",
                   aTabBrowser, null, iFrame1Listener, 250, 80);
}


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

// Create array of components.
var gComponentsArray = [GmailService];

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

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


/**************************************************************************
 * GMAIL API Start
 **************************************************************************/


function GMailAPI() {
  this.shortName = CLASS_SHORT_NAME;
  FlockSvcUtils.getLogger(this).info(".init() - API");
  FlockSvcUtils.getWD(this);
}


/**************************************************************************
 * Modified from Firefox extension GMail Notifier
 * https://addons.mozilla.org/en-US/firefox/addon/173
 * The Initial Developer of the Original Code is
 * Doron Rosenberg.
 **************************************************************************/


GMailAPI.prototype._unreadMessagesCallback =
function gmAPI__unreadMessagesCallback(aData, aListener, aThis) {
  var sandbox = new CU.Sandbox("about:blank");

  var val;
  var newDS = false;

  if (aData.match(/\["ds",\[\[/)) {
    val = aData.match(/\["ds",.*\n(.*]\n)+\]/gm);
    newDS = true;
  } else {
    val = aData.match(/\["ds",.*\]/);
  }

  var ts = aData.match(/\["ts",.*\]/gm);
  var myArray = CU.evalInSandbox(ts[0], sandbox);
  var totalUnread = myArray[3];

  // handle inbox
  myArray = CU.evalInSandbox(val[0], sandbox);
  var inboxUnread = 0;
  if (newDS) {
    inboxUnread = myArray[1][0][1];
  } else {
    inboxUnread = myArray[1];
  }

  function loadMessages(aMessageArr) {
    // We should only be dealing with 't' messages for now as they represent
    // unread messages.
    if (aMessageArr[0] != "t") {
      return; 
    }

    var inboxUrl = aThis._WebDetective.getString(CLASS_SHORT_NAME,
                                                 "inboxURL",
                                                 DOMAIN);

    // Ensure we're displaying UTF-8 strings to handle non-English characters
    // as well
    function _convertToUTF8(aStr) {
      var val = aStr;
      var conv = CC["@mozilla.org/intl/utf8converterservice;1"]
                 .getService(CI.nsIUTF8ConverterService);
      try {
        val = conv.convertStringToUTF8(val, "utf-8", false);
      } catch (ex) {
        var logger = CC["@flock.com/logger;1"].getService(CI.flockILogger);
        logger.init("Gmail unread message conversion error: " + ex);
      }
      return flockXMLDecode(val);
    }

    var newestMessageDate = 0;
    var messages = [];
    for (var i = 1; i < aMessageArr.length; i++) {
      var msgs = aMessageArr[i];
      if (msgs.length < 7) {
        continue;
      }
      // Because Gmail lists all people in the email/conversation (From,
      // separated by commas), only list the last person in the email list
      var froms = msgs[4].split(",");
      var from = froms[froms.length - 1].match(/<.>(.+)<\/.>/);
      var title = msgs[6].match(/<.>(.+)<\/.>/);
      messages.push({
        from: _convertToUTF8(from[1]),
        subject: _convertToUTF8(title[1]),
        url: inboxUrl + "/" + msgs[0]
      });

      // Retrieve the date of the newest message
      if (!newestMessageDate) {
        // Bug 12211: Gmail does not provide the same date format across
        // different languages so we will use the message id instead as
        // a fake "message date" since all indicators point to this value
        // always incrementing upwards
        newestMessageDate = parseInt(msgs[0], 16);
      }
    }

    var unreadMessages = {
      lastCheckedDate: Date.now(),
      newestMessageDate: newestMessageDate,
      unreadMessageCount: inboxUnread,
      messages: messages
    };
    aListener.onSuccess(JSON.toString(unreadMessages));
  }

  var t = aData.match(/\["t",.*\n(.*]\n)+\]/);
  if (t) {
    var messageArray = CU.evalInSandbox(t[0], sandbox);
    loadMessages(messageArray);
  } else {
    aListener.onSuccess(webmailBackend.createEmptyMessageAsJSON());
  }
}

GMailAPI.prototype.getUnreadMessages =
function gmAPI_getUnreadMessages(aUserName, aListener)
{
  var unreadQueryUrl = GMAIL_URL +
                       this._WebDetective.getString(CLASS_SHORT_NAME,
                                                    "unreadQuery",
                                                    DOMAIN);
  this._call(aUserName, unreadQueryUrl,
             null, null, GMAIL_URL, this._unreadMessagesCallback, aListener);
}

GMailAPI.prototype._call =
function gmAPI__call(aUsername, aURL, aPostData,
                     aCookieData, aReferrer,
                     aCallbackFunc, aListener) {
  // the IO service
  var ioService = CC["@mozilla.org/network/io-service;1"]
                  .getService(CI.nsIIOService);

  // create an nsIURI
  var uri = ioService.newURI(aURL, null, null);

  //nsIInputStream
  var uploadStream = CC["@mozilla.org/io/string-input-stream;1"]
                     .createInstance(CI.nsIStringInputStream);

  if (aPostData) {
    uploadStream.setData(aPostData, aPostData.length);
  }

  // clean up if old channel exists.  Should never happen really!
  if (this.channel) {
    this.channel.cancel(CR.NS_BINDING_ABORTED);
    this.channel = null;
  }

  // get a channel for that nsIURI
  this.channel = ioService.newChannelFromURI(uri);

  // get a httpchannel and make it a post
  var httpChannel = this.channel.QueryInterface(CI.nsIHttpChannel);

  // set a referrer
  if (aReferrer) {
    var referrerUri = ioService.newURI(aReferrer, null, null);
    httpChannel.referrer = referrerUri;
  }

  if (aPostData) {
    var uploadChannel = this.channel.QueryInterface(CI.nsIUploadChannel);
    uploadChannel.setUploadStream(uploadStream,
                                  "application/x-www-form-urlencoded",
                                  -1);

    // order important - setUploadStream resets to get/put
    httpChannel.requestMethod = "POST";
  }

  if (aCookieData) {
    // httpChannel.setRequestHeader("Cookie", aCookieData, false);
    for (var run = 0; run < aCookieData.length; run++) {
      httpChannel.setRequestHeader("Cookie", aCookieData[run], true);
    }
  }

  var observer = new this.observer(aCallbackFunc, aUsername, this, aListener);

  this._logger.info("observer notificationCallbacks start");
  this.channel.notificationCallbacks = observer;
  this._logger.info("observer notificationCallbacks done");
  this.channel.asyncOpen(observer, null);
  this._logger.info("observer asyncOpen done");
}

GMailAPI.prototype.observer =
function gmAPI_observer(aCallbackFunc, aUsername, aThis, aListener) {
  aThis._logger.info("observer('" + aUsername + "')");
  var myThis = aThis;

  return ({
    data : "",

    onStartRequest : function gmAPI_obs_onStartRequest(aRequest, aContext) {
      this.data = "";
    },

    onDataAvailable : function gmAPI_obs_onDataAvailable(aRequest,
                                                         aContext,
                                                         aStream,
                                                         aSourceOffset,
                                                         aLength)
    {
      var scriptableInputStream = CC["@mozilla.org/scriptableinputstream;1"]
                                  .createInstance(CI.nsIScriptableInputStream);
      scriptableInputStream.init(aStream);

      this.data += scriptableInputStream.read(aLength);
      myThis._logger.info("observer " + this.data);
    },

    onStopRequest : function gmAPI_obs_onStopRequest(aRequest,
                                                     aContext,
                                                     aStatus)
    {
      aCallbackFunc(this.data, aListener, myThis);
    },

    onChannelRedirect : function gmAPI_obs_onChannelRedirect(aOldChannel,
                                                             aNewChannel,
                                                             aFlags)
    {
      if (aOldChannel == myThis.channel) {
        myThis.channel = aNewChannel;
      }
    },

    // nsIInterfaceRequestor
    getInterface : function gmAPI_obs_getInterface(aIID) {
      try {
        return this.QueryInterface(aIID);
      } catch (e) {
        throw CR.NS_NOINTERFACE;
      }
    },

    // nsIProgressEventSink (to shut up annoying debug exceptions
    onProgress : function gmAPI_obs_onProgress(aRequest, aContext,
                                               aProgress, aProgressMax) {},

    onStatus : function gmAPI_obs_onStatus(aRequest, aContext,
                                           aStatus, aStatusArg) {},

    // nsIHttpEventSink (to shut up annoying debug exceptions
    onRedirect : function gmAPI_obs_onRedirect(aOldChannel, aNewChannel) {},

    QueryInterface : function gmAPI_obs_QueryInterface(aIID) {
      if (aIID.equals(CI.nsISupports) ||
          aIID.equals(CI.nsIDocShell) ||
          aIID.equals(CI.nsIInterfaceRequestor) ||
          aIID.equals(CI.nsIChannelEventSink) ||
          aIID.equals(CI.nsISupportsWeakReference) ||
          aIID.equals(CI.nsIProgressEventSink) ||
          aIID.equals(CI.nsIPrompt) ||
          aIID.equals(CI.nsIHttpEventSink) ||
          aIID.equals(CI.nsIDocShellTreeItem) ||
          aIID.equals(CI.nsIStreamListener))
      {
        return this;
      }

      throw CR.NS_NOINTERFACE;
    }
  });
}


/**************************************************************************
 * GMAIL API End
 **************************************************************************/

