Bug 1155491 - Support autoconfig and manual config of gmail IMAP OAuth2 authentication, r=jcranmer, a=rkent
authorR Kent James <rkent@caspia.com>
Thu, 23 Apr 2015 15:32:55 -0700
changeset 25970 8f1e54ffa545f7dee46b901b1252b6cfb163b58a
parent 25969 409c4c0fd0dd6673d8fd1c65cdca8aa97d291223
child 25971 60320b7d5e6caa8f6ccd6ea9eff5987aadafb7cb
push id1850
push userclokep@gmail.com
push dateWed, 08 Mar 2017 19:29:12 +0000
treeherdercomm-esr52@028df196b2d9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjcranmer, rkent
bugs1155491
Bug 1155491 - Support autoconfig and manual config of gmail IMAP OAuth2 authentication, r=jcranmer, a=rkent
mailnews/base/prefs/content/accountcreation/accountConfig.js
mailnews/base/prefs/content/accountcreation/createInBackend.js
mailnews/base/prefs/content/accountcreation/emailWizard.js
mailnews/base/prefs/content/accountcreation/emailWizard.xul
mailnews/base/prefs/content/accountcreation/guessConfig.js
mailnews/base/prefs/content/accountcreation/readFromXML.js
mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js
mailnews/base/prefs/content/accountcreation/util.js
mailnews/base/prefs/content/accountcreation/verifyConfig.js
mailnews/base/src/msgOAuth2Module.js
mailnews/base/util/OAuth2.jsm
mailnews/base/util/OAuth2Providers.jsm
mailnews/base/util/moz.build
--- a/mailnews/base/prefs/content/accountcreation/accountConfig.js
+++ b/mailnews/base/prefs/content/accountcreation/accountConfig.js
@@ -45,16 +45,18 @@ AccountConfig.prototype =
   /**
    * Other servers which can be used instead of |incoming|,
    * in order of decreasing preference.
    * (|incoming| itself should not be included here.)
    * { Array of incoming/createNewIncoming() }
    */
   incomingAlternatives : null,
   outgoingAlternatives : null,
+  // OAuth2 configuration, if needed.
+  oauthSettings : null,
   // just an internal string to refer to this. Do not show to user.
   id : null,
   // who created the config.
   // { one of kSource* }
   source : 0,
   displayName : null,
   // { Array of { varname (value without %), displayName, exampleValue } }
   inputFields : null,
--- a/mailnews/base/prefs/content/accountcreation/createInBackend.js
+++ b/mailnews/base/prefs/content/accountcreation/createInBackend.js
@@ -23,16 +23,21 @@ function createAccountInBackend(config)
       config.incoming.hostname,
       sanitize.enum(config.incoming.type, ["pop3", "imap", "nntp"]));
   inServer.port = config.incoming.port;
   inServer.authMethod = config.incoming.auth;
   inServer.password = config.incoming.password;
   if (config.rememberPassword && config.incoming.password.length)
     rememberPassword(inServer, config.incoming.password);
 
+  if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+    inServer.setCharValue("oauth2.scope", config.oauthSettings.scope);
+    inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer);
+  }
+
   // SSL
   if (config.incoming.socketType == 1) // plain
     inServer.socketType = Ci.nsMsgSocketType.plain;
   else if (config.incoming.socketType == 2) // SSL / TLS
     inServer.socketType = Ci.nsMsgSocketType.SSL;
   else if (config.incoming.socketType == 3) // STARTTLS
     inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
   //inServer.prettyName = config.displayName;
@@ -97,16 +102,24 @@ function createAccountInBackend(config)
     if (config.outgoing.auth > 1)
     {
       outServer.username = username;
       outServer.password = config.incoming.password;
       if (config.rememberPassword && config.incoming.password.length)
         rememberPassword(outServer, config.incoming.password);
     }
 
+    if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+      let pref = "mail.smtpserver." + outServer.key + ".";
+      Services.prefs.setCharPref(pref + "oauth2.scope",
+                                 config.oauthSettings.scope);
+      Services.prefs.setCharPref(pref + "oauth2.issuer",
+                                 config.oauthSettings.issuer);
+    }
+
     if (config.outgoing.socketType == 1) // no SSL
       outServer.socketType = Ci.nsMsgSocketType.plain;
     else if (config.outgoing.socketType == 2) // SSL / TLS
       outServer.socketType = Ci.nsMsgSocketType.SSL;
     else if (config.outgoing.socketType == 3) // STARTTLS
       outServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
 
     // API problem: <http://mxr.mozilla.org/seamonkey/source/mailnews/compose/public/nsISmtpServer.idl#93>
--- a/mailnews/base/prefs/content/accountcreation/emailWizard.js
+++ b/mailnews/base/prefs/content/accountcreation/emailWizard.js
@@ -1,16 +1,17 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource:///modules/mailServices.js");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource:///modules/hostnameUtils.jsm");
+Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
 
 /**
  * This is the dialog opened by menu File | New account | Mail... .
  *
  * It gets the user's realname, email address and password,
  * and tries to automatically configure the account from that,
  * using various mechanisms. If all fails, the user can enter/edit
  * the config, then we create the account.
@@ -29,18 +30,20 @@ Components.utils.import("resource:///mod
  * - let user verify and maybe edit the server names and ports
  * - If user clicks OK, create the account
  */
 
 
 // from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html
 var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i;
 
-Cu.import("resource:///modules/gloda/log4moz.js");
-let gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+if (typeof gEmailWizardLogger == "undefined") {
+  Cu.import("resource:///modules/gloda/log4moz.js");
+  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
 
 var gStringsBundle;
 var gMessengerBundle;
 var gBrandShortName;
 
 /*********************
 TODO for bug 549045
 - autodetect protocol
@@ -176,23 +179,25 @@ EmailConfigWizard.prototype =
     gBrandShortName = e("bundle_brand").getString("brandShortName");
 
     setLabelFromStringBundle("in-authMethod-password-cleartext",
         "authPasswordCleartextViaSSL"); // will warn about insecure later
     setLabelFromStringBundle("in-authMethod-password-encrypted",
         "authPasswordEncrypted");
     setLabelFromStringBundle("in-authMethod-kerberos", "authKerberos");
     setLabelFromStringBundle("in-authMethod-ntlm", "authNTLM");
+    setLabelFromStringBundle("in-authMethod-oauth2", "authOAuth2");
     setLabelFromStringBundle("out-authMethod-no", "authNo");
     setLabelFromStringBundle("out-authMethod-password-cleartext",
         "authPasswordCleartextViaSSL"); // will warn about insecure later
     setLabelFromStringBundle("out-authMethod-password-encrypted",
         "authPasswordEncrypted");
     setLabelFromStringBundle("out-authMethod-kerberos", "authKerberos");
     setLabelFromStringBundle("out-authMethod-ntlm", "authNTLM");
+    setLabelFromStringBundle("out-authMethod-oauth2", "authOAuth2");
 
     e("incoming_port").value = gStringsBundle.getString("port_auto");
     this.fillPortDropdown("smtp");
 
     // If the account provisioner is preffed off, don't display
     // the account provisioner button.
     if (!Services.prefs.getBoolPref("mail.provider.enabled"))
       _hide("provisioner_button");
@@ -660,17 +665,17 @@ EmailConfigWizard.prototype =
    * This displays the config to the user.
    */
   foundConfig : function(config)
   {
     gEmailWizardLogger.info("foundConfig()");
     assert(config instanceof AccountConfig,
         "BUG: Arg 'config' needs to be an AccountConfig object");
 
-    this._haveValidConfigForDomain = this._email.split("@")[1];;
+    this._haveValidConfigForDomain = this._email.split("@")[1];
 
     if (!this._realname || !this._email) {
       return;
     }
     this._foundConfig2(config);
   },
 
   // Continuation of foundConfig2() after custom fields.
@@ -787,16 +792,17 @@ EmailConfigWizard.prototype =
           unknownString);
       let host = server.hostname +
           (isStandardPort(server.port) ? "" : ":" + server.port);
       let ssl = gStringsBundle.getString(sanitize.translate(server.socketType,
           { 1 : "resultNoEncryption", 2 : "resultSSL", 3 : "resultSTARTTLS" }),
           unknownString);
       let certStatus = gStringsBundle.getString(server.badCert ?
           "resultSSLCertWeak" : "resultSSLCertOK");
+      // TODO: we should really also display authentication method here.
       return gStringsBundle.getFormattedString(stringName,
           [ type, host, ssl, certStatus ]);
     };
 
     var incomingResult = unknownString;
     if (configFilledIn.incoming.hostname) {
       incomingResult = _makeHostDisplayString(configFilledIn.incoming,
           "resultIncoming");
@@ -1003,47 +1009,74 @@ EmailConfigWizard.prototype =
 
     // incoming server
     e("incoming_protocol").value = sanitize.translate(config.incoming.type,
                                                 { "imap" : 1, "pop3" : 2 }, 1);
     e("incoming_hostname").value = config.incoming.hostname;
     e("incoming_ssl").value = sanitize.enum(config.incoming.socketType,
                                             [ 0, 1, 2, 3 ], 0);
     e("incoming_authMethod").value = sanitize.enum(config.incoming.auth,
-                                                   [ 0, 3, 4, 5, 6 ], 0);
+                                                   [ 0, 3, 4, 5, 6, 10 ], 0);
     e("incoming_username").value = config.incoming.username;
     if (config.incoming.port) {
       e("incoming_port").value = config.incoming.port;
     } else {
       this.adjustIncomingPortToSSLAndProtocol(config);
     }
     this.fillPortDropdown(config.incoming.type);
 
+    // If the hostname supports OAuth2 and imap is enabled, enable OAuth2.
+    let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname);
+    gEmailWizardLogger.info("OAuth2 details for incoming hostname " +
+                            config.incoming.hostname + " is " + iDetails);
+    e("in-authMethod-oauth2").hidden = !(iDetails && e("incoming_protocol").value == 1);
+    if (!e("in-authMethod-oauth2").hidden) {
+      config.oauthSettings = {};
+      [config.oauthSettings.issuer, config.oauthSettings.scope] = iDetails;
+      // oauthsettings are not stored nor changable in the user interface, so just
+      // store them in the base configuration.
+      this._currentConfig.oauthSettings = config.oauthSettings;
+    }
+
     // outgoing server
     e("outgoing_hostname").value = config.outgoing.hostname;
     e("outgoing_username").value = config.outgoing.username;
     // While sameInOutUsernames is true we synchronize values of incoming
     // and outgoing username.
     this.sameInOutUsernames = true;
     e("outgoing_ssl").value = sanitize.enum(config.outgoing.socketType,
                                             [ 0, 1, 2, 3 ], 0);
     e("outgoing_authMethod").value = sanitize.enum(config.outgoing.auth,
-                                                   [ 0, 1, 3, 4, 5, 6 ], 0);
+                                                   [ 0, 1, 3, 4, 5, 6, 10 ], 0);
     if (config.outgoing.port) {
       e("outgoing_port").value = config.outgoing.port;
     } else {
       this.adjustOutgoingPortToSSLAndProtocol(config);
     }
+
+    // If the hostname supports OAuth2 and imap is enabled, enable OAuth2.
+    let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname);
+    gEmailWizardLogger.info("OAuth2 details for outgoing hostname " +
+                            config.outgoing.hostname + " is " + oDetails);
+    e("out-authMethod-oauth2").hidden = !oDetails;
+    if (!e("out-authMethod-oauth2").hidden) {
+      config.oauthSettings = {};
+      [config.oauthSettings.issuer, config.oauthSettings.scope] = oDetails;
+      // oauthsettings are not stored nor changable in the user interface, so just
+      // store them in the base configuration.
+      this._currentConfig.oauthSettings = config.oauthSettings;
+    }
+
     // populate fields even if existingServerKey, in case user changes back
-
     if (config.outgoing.existingServerKey) {
       let menulist = e("outgoing_hostname");
       // We can't use menulist.value = config.outgoing.existingServerKey
       // because would overwrite the text field, so have to do it manually:
-      for each (let menuitem in e("outgoing_hostname_popup").childNodes) {
+      let menuitems = menulist.menupopup.childNodes;
+      for (let menuitem of menuitems) {
         if (menuitem.serverKey == config.outgoing.existingServerKey) {
           menulist.selectedItem = menuitem;
           break;
         }
       }
     }
     this.onChangedOutgoingDropdown(); // show/hide outgoing port, SSL, ...
   },
@@ -1575,16 +1608,22 @@ EmailConfigWizard.prototype =
                          "password_ok" : null);
 
         // the auth might have changed, so we
         // should back-port it to the current config.
         self._currentConfig.incoming.auth = successfulConfig.incoming.auth;
         self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth;
         self._currentConfig.incoming.username = successfulConfig.incoming.username;
         self._currentConfig.outgoing.username = successfulConfig.outgoing.username;
+
+        // We loaded dynamic client registration, fill this data back in to the
+        // config set.
+        if (successfulConfig.oauthSettings)
+          self._currentConfig.oauthSettings = successfulConfig.oauthSettings;
+
         self.finish();
       },
       function(e) // failed
       {
         self.showErrorStatus("config_unverifiable");
         // TODO bug 555448: wrong error msg, there may be a 1000 other
         // reasons why this failed, and this is misleading users.
         self.setError("passworderror", "user_pass_invalid");
--- a/mailnews/base/prefs/content/accountcreation/emailWizard.xul
+++ b/mailnews/base/prefs/content/accountcreation/emailWizard.xul
@@ -302,16 +302,17 @@
                 <menuitem label="&autodetect.label;" value="0"/>
                 <!-- values defined in nsMsgAuthMethod -->
                 <!-- labels set from messenger.properties
                      to avoid duplication -->
                 <menuitem id="in-authMethod-password-cleartext" value="3"/>
                 <menuitem id="in-authMethod-password-encrypted" value="4"/>
                 <menuitem id="in-authMethod-kerberos" value="5"/>
                 <menuitem id="in-authMethod-ntlm" value="6"/>
+                <menuitem id="in-authMethod-oauth2" value="10" hidden="true"/>
               </menupopup>
             </menulist>
           </row>
           <row id="outgoing_server_area" align="center">
             <label class="textbox-label"
                    value="&outgoing.label;"
                    control="outgoing_hostname"/>
             <label id="outgoing_protocol"
@@ -351,16 +352,17 @@
               <menupopup>
                 <menuitem label="&autodetect.label;" value="0"/>
                 <!-- @see incoming -->
                 <menuitem id="out-authMethod-no" value="1"/>
                 <menuitem id="out-authMethod-password-cleartext" value="3"/>
                 <menuitem id="out-authMethod-password-encrypted" value="4"/>
                 <menuitem id="out-authMethod-kerberos" value="5"/>
                 <menuitem id="out-authMethod-ntlm" value="6"/>
+                <menuitem id="out-authMethod-oauth2" value="10" hidden="true"/>
               </menupopup>
             </menulist>
           </row>
           <row id="username_area" align="center">
             <label class="textbox-label"
                    value="&username.label;"/>
             <label class="columnHeader"
                    value="&incoming.label;"
--- a/mailnews/base/prefs/content/accountcreation/guessConfig.js
+++ b/mailnews/base/prefs/content/accountcreation/guessConfig.js
@@ -54,16 +54,23 @@ var outgoingDone = false;
  * @result {Abortable} Allows you to cancel the guess
  */
 function guessConfig(domain, progressCallback, successCallback, errorCallback,
                      resultConfig, which)
 {
   assert(typeof(progressCallback) == "function", "need progressCallback");
   assert(typeof(successCallback) == "function", "need successCallback");
   assert(typeof(errorCallback) == "function", "need errorCallback");
+
+  // Servers that we know enough that they support OAuth2 do not need guessing.
+  if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) {
+    successCallback(resultConfig);
+    return null;
+  }
+
   if (!resultConfig)
     resultConfig = new AccountConfig();
   resultConfig.source = AccountConfig.kSourceGuess;
 
   if (!Services.prefs.getBoolPref(
       "mailnews.auto_config.guess.enabled")) {
     errorCallback("Guessing config disabled per user preference");
     return;
--- a/mailnews/base/prefs/content/accountcreation/readFromXML.js
+++ b/mailnews/base/prefs/content/accountcreation/readFromXML.js
@@ -49,16 +49,26 @@ function readFromXML(clientConfigXML)
     try {
       d.domains.push(sanitize.hostname(domain));
     } catch (e) { logException(e); exception = e; }
   }
   if (d.domains.length == 0)
     throw exception ? exception : "need proper <domain> in XML";
   exception = null;
 
+  let oauthSettings = null;
+  if ("oauth2Settings" in clientConfigXML.clientConfig) {
+    oauthSettings = {};
+    oauthSettings.scope = sanitize.nonemptystring(
+      clientConfigXML.clientConfig.oauth2Settings.scope);
+    if (!oauthSettings.scope)
+      throw new Error("Malformed oauth2Settings in configuration XML");
+    d.oauthSettings = oauthSettings;
+  }
+
   // incoming server
   for (let iX of array_or_undef(xml.$incomingServer)) // input (XML)
   {
     let iO = d.createNewIncoming(); // output (object)
     try {
       // throws if not supported
       iO.type = sanitize.enum(iX["@type"], ["pop3", "imap", "nntp"]);
       iO.hostname = sanitize.hostname(iX.hostname);
@@ -89,17 +99,23 @@ function readFromXML(clientConfigXML)
           iO.auth = sanitize.translate(iXauth,
               { "password-cleartext" : Ci.nsMsgAuthMethod.passwordCleartext,
                 // @deprecated TODO remove
                 "plain" : Ci.nsMsgAuthMethod.passwordCleartext,
                 "password-encrypted" : Ci.nsMsgAuthMethod.passwordEncrypted,
                 // @deprecated TODO remove
                 "secure" : Ci.nsMsgAuthMethod.passwordEncrypted,
                 "GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI,
-                "NTLM" : Ci.nsMsgAuthMethod.NTLM });
+                "NTLM" : Ci.nsMsgAuthMethod.NTLM,
+                "OAuth2" : Ci.nsMsgAuthMethod.OAuth2 });
+
+          // If we're using OAuth2, but don't have working settings, bail.
+          if (iO.auth == Ci.nsMsgAuthMethod.OAuth2 && !oauthSettings)
+            continue;
+
           break; // take first that we support
         } catch (e) { exception = e; }
       }
       if (!iO.auth)
         throw exception ? exception : "need proper <authentication> in XML";
       exception = null;
 
       // defaults are in accountConfig.js
@@ -170,17 +186,23 @@ function readFromXML(clientConfigXML)
                 "password-cleartext" : Ci.nsMsgAuthMethod.passwordCleartext,
                 // @deprecated TODO remove
                 "plain" : Ci.nsMsgAuthMethod.passwordCleartext,
                 "password-encrypted" : Ci.nsMsgAuthMethod.passwordEncrypted,
                 // @deprecated TODO remove
                 "secure" : Ci.nsMsgAuthMethod.passwordEncrypted,
                 "GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI,
                 "NTLM" : Ci.nsMsgAuthMethod.NTLM,
+                "OAuth2" : Ci.nsMsgAuthMethod.OAuth2,
               });
+
+          // If we're using OAuth2, but don't have working settings, bail.
+          if (oO.auth == Ci.nsMsgAuthMethod.OAuth2 && !oauthSettings)
+            continue;
+
           break; // take first that we support
         } catch (e) { exception = e; }
       }
       if (!oO.auth)
         throw exception ? exception : "need proper <authentication> in XML";
       exception = null;
 
       if ("username" in oX ||
--- a/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js
+++ b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js
@@ -113,17 +113,17 @@ var sanitize =
   url : function (unchecked)
   {
     var str =  this.string(unchecked);
     if (!str.startsWith("http") && !str.startsWith("https"))
       throw new MalformedException("url_scheme.error", unchecked);
 
     var uri;
     try {
-      uri = ioService().newURI(str, null, null);
+      uri = Services.io.newURI(str, null, null);
       uri = uri.QueryInterface(Ci.nsIURL);
     } catch (e) {
       throw new MalformedException("url_parsing.error", unchecked);
     }
 
     if (uri.scheme != "http" && uri.scheme != "https")
       throw new MalformedException("url_scheme.error", unchecked);
 
--- a/mailnews/base/prefs/content/accountcreation/util.js
+++ b/mailnews/base/prefs/content/accountcreation/util.js
@@ -243,21 +243,23 @@ function deepCopy(org)
   var result = new Object();
   if (typeof(org.length) != "undefined")
     var result = new Array();
   for (var prop in org)
     result[prop] = deepCopy(org[prop]);
   return result;
 }
 
-let kDebug = false;
+if (typeof gEmailWizardLogger == "undefined") {
+  Cu.import("resource:///modules/gloda/log4moz.js");
+  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
 function ddump(text)
 {
-  if (kDebug)
-    dump(text + "\n");
+  gEmailWizardLogger.info(text);
 }
 
 function debugObject(obj, name, maxDepth, curDepth)
 {
   if (curDepth == undefined)
     curDepth = 0;
   if (maxDepth != undefined && curDepth > maxDepth)
     return "";
--- a/mailnews/base/prefs/content/accountcreation/verifyConfig.js
+++ b/mailnews/base/prefs/content/accountcreation/verifyConfig.js
@@ -25,16 +25,22 @@
  * @param errorCallback function(ex)
  *   Called when we could guess not the config, either
  *   because we have not found anything or
  *   because there was an error (e.g. no network connection).
  *   The ex.message will contain a user-presentable message.
  */
 
 Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
+
+if (typeof gEmailWizardLogger == "undefined") {
+  Cu.import("resource:///modules/gloda/log4moz.js");
+  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
 
 function verifyConfig(config, alter, msgWindow, successCallback, errorCallback)
 {
   ddump(debugObject(config, "config", 3));
   assert(config instanceof AccountConfig,
          "BUG: Arg 'config' needs to be an AccountConfig object");
   assert(typeof(alter) == "boolean");
   assert(typeof(successCallback) == "function");
@@ -58,52 +64,82 @@ function verifyConfig(config, alter, msg
   inServer.port = config.incoming.port;
   inServer.password = config.incoming.password;
   if (config.incoming.socketType == 1) // plain
     inServer.socketType = Ci.nsMsgSocketType.plain;
   else if (config.incoming.socketType == 2) // SSL
     inServer.socketType = Ci.nsMsgSocketType.SSL;
   else if (config.incoming.socketType == 3) // STARTTLS
     inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+
+  gEmailWizardLogger.info("Setting incoming server authMethod to " +
+                           config.incoming.auth);
   inServer.authMethod = config.incoming.auth;
 
   try {
-    if (inServer.password)
+    // Lookup issuer if needed.
+    if (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 ||
+        config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2) {
+      if (!config.oauthSettings.issuer || !config.oauthSettings.scope) {
+        // lookup issuer or scope from hostname
+        let hostname = (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) ?
+                       config.incoming.hostname : config.outgoing.hostname;
+        let hostDetails = OAuth2Providers.getHostnameDetails(hostname);
+        if (hostDetails)
+          [config.oauthSettings.issuer, config.oauthSettings.scope] = hostDetails;
+        if (!config.oauthSettings.issuer || !config.oauthSettings.scope)
+          throw "Could not get issuer for oauth2 authentication";
+      }
+      gEmailWizardLogger.info("Saving oauth parameters for issuer " +
+                               config.oauthSettings.issuer);
+      inServer.setCharValue("oauth2.scope", config.oauthSettings.scope);
+      inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer);
+      gEmailWizardLogger.info("OAuth2 issuer, scope is " +
+                              config.oauthSettings.issuer + ", " + config.oauthSettings.scope);
+    }
+
+    if (inServer.password ||
+        inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2)
       verifyLogon(config, inServer, alter, msgWindow,
                   successCallback, errorCallback);
     else {
       // Avoid pref pollution, clear out server prefs.
       MailServices.accounts.removeIncomingServer(inServer, true);
       successCallback(config);
     }
-  } catch (e) {
-    ddump("ERROR: verify logon shouldn't have failed");
-    errorCallback(e);
-    throw(e);
+    return;
+  }
+  catch (e) {
+    gEmailWizardLogger.error("ERROR: verify logon shouldn't have failed");
   }
-};
+  // Avoid pref pollution, clear out server prefs.
+  MailServices.accounts.removeIncomingServer(inServer, true);
+  errorCallback(e);
+}
 
 function verifyLogon(config, inServer, alter, msgWindow, successCallback,
                      errorCallback)
 {
+  gEmailWizardLogger.info("verifyLogon for server at " + inServer.hostName);
   // hack - save away the old callbacks.
   let saveCallbacks = msgWindow.notificationCallbacks;
   // set our own callbacks - this works because verifyLogon will
   // synchronously create the transport and use the notification callbacks.
   let listener = new urlListener(config, inServer, alter, msgWindow,
                                  successCallback, errorCallback);
   // our listener listens both for the url and cert errors.
   msgWindow.notificationCallbacks = listener;
   // try to work around bug where backend is clearing password.
   try {
     inServer.password = config.incoming.password;
     let uri = inServer.verifyLogon(listener, msgWindow);
     // clear msgWindow so url won't prompt for passwords.
     uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null;
   }
+  catch (e) { gEmailWizardLogger.error("verifyLogon failed: " + e); throw e;}
   finally {
     // restore them
     msgWindow.notificationCallbacks = saveCallbacks;
   }
 }
 
 /**
  * The url listener also implements nsIBadCertListener2.  Its job is to prevent
--- a/mailnews/base/src/msgOAuth2Module.js
+++ b/mailnews/base/src/msgOAuth2Module.js
@@ -4,42 +4,35 @@
 
 "use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 Components.utils.import("resource://gre/modules/OAuth2.jsm");
+Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
 Components.utils.import("resource://gre/modules/Preferences.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 function OAuth2Module() {
   this._refreshToken = '';
 }
 OAuth2Module.prototype = {
   // XPCOM registration stuff
   QueryInterface: XPCOMUtils.generateQI([Ci.msgIOAuth2Module]),
   classID: Components.ID("{b63d8e4c-bf60-439b-be0e-7c9f67291042}"),
 
   _loadOAuthClientDetails(aIssuer) {
-    if (aIssuer == "accounts.google.com") {
-      // For the moment, these details are hard-coded, since Google does not
-      // provide dynamic client registration. Don't copy these values for your
-      // own application--register it yourself. This code (and possibly even the
-      // registration itself) will disappear when this is switched to dynamic
-      // client registration.
-      this._appKey = '406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com';
-      this._appSecret = 'kSmqreRr0qwBWJgbf5Y-PjSU';
-      this._authURI = "https://accounts.google.com/o/oauth2/auth";
-      this._tokenURI = "https://www.googleapis.com/oauth2/v3/token";
-    } else {
+    let details = OAuth2Providers.getIssuerDetails(aIssuer);
+    if (details)
+      [this._appKey, this._appSecret, this._authURI, this._tokenURI] = details;
+    else
       throw Cr.NS_ERROR_INVALID_ARGUMENT;
-    }
   },
   initFromSmtp(aServer) {
     return this._initPrefs("mail.smtpserver." + aServer.key + ".",
       aServer.username, aServer.hostname);
   },
   initFromMail(aServer) {
     return this._initPrefs("mail.server." + aServer.key + ".",
       aServer.username, aServer.realHostName);
@@ -48,20 +41,20 @@ OAuth2Module.prototype = {
     // Load all of the parameters from preferences.
     let issuer = Preferences.get(root + "oauth2.issuer", "");
     let scope = Preferences.get(root + "oauth2.scope", "");
 
     // These properties are absolutely essential to OAuth2 support. If we don't
     // have them, we don't support OAuth2.
     if (!issuer || !scope) {
       // Since we currently only support gmail, init values if server matches.
-      if (aHostname == "imap.googlemail.com" || aHostname == "smtp.googlemail.com")
+      let details = OAuth2Providers.getHostnameDetails(aHostname);
+      if (details)
       {
-        issuer = "accounts.google.com";
-        scope = "http://mail.google.com/";
+        [issuer, scope] = details;
         Preferences.set(root + "oauth2.issuer", issuer);
         Preferences.set(root + "oauth2.scope", scope);
       }
       else
         return false;
     }
 
     // Find the app key we need for the OAuth2 string. Eventually, this should
--- a/mailnews/base/util/OAuth2.jsm
+++ b/mailnews/base/util/OAuth2.jsm
@@ -51,35 +51,33 @@ OAuth2.prototype = {
     requestWindowTitle: "",
     scope: null,
 
     accessToken: null,
     refreshToken: null,
     tokenExpires: 0,
 
     connect: function connect(aSuccess, aFailure, aWithUI, aRefresh) {
-        if (gConnecting[this.authURI]) {
-            aFailure("Window already open");
-            return;
-        }
 
         this.connectSuccessCallback = aSuccess;
         this.connectFailureCallback = aFailure;
 
         if (!aRefresh && this.accessToken) {
             aSuccess();
         } else if (this.refreshToken) {
-            gConnecting[this.authURI] = true;
             this.requestAccessToken(this.refreshToken, OAuth2.CODE_REFRESH);
         } else {
             if (!aWithUI) {
                 aFailure('{ "error": "auth_noui" }');
                 return;
             }
-            gConnecting[this.authURI] = true;
+            if (gConnecting[this.authURI]) {
+                aFailure("Window already open");
+                return;
+            }
             this.requestAuthorization();
         }
     },
 
     requestAuthorization: function requestAuthorization() {
         let params = [
             ["response_type", this.responseType],
             ["client_id", this.consumerKey],
@@ -151,19 +149,21 @@ OAuth2.prototype = {
                 };
                 aWebProgress.addProgressListener(this._listener,
                                                  Ci.nsIWebProgress.NOTIFY_ALL);
                 aWindow.document.title = this.account.requestWindowTitle;
             }
         };
 
         this.wrappedJSObject = this._browserRequest;
+        gConnecting[this.authURI] = true;
         Services.ww.openWindow(null, this.requestWindowURI, null, this.requestWindowFeatures, this);
     },
     finishAuthorizationRequest: function() {
+        gConnecting[this.authURI] = false;
         if (!("_browserRequest" in this)) {
             return;
         }
 
         this._browserRequest._active = false;
         if ("_listener" in this._browserRequest) {
             this._browserRequest._listener._cleanUp();
         }
@@ -178,17 +178,16 @@ OAuth2.prototype = {
         } else if (this.responseType == "token") {
             this.onAccessTokenReceived(JSON.stringify(results));
         }
         else
           this.onAuthorizationFailed(null, aData);
     },
 
     onAuthorizationFailed: function(aError, aData) {
-        gConnecting[this.authURI] = false;
         this.connectFailureCallback(aData);
     },
 
     requestAccessToken: function requestAccessToken(aCode, aType) {
         let params = [
             ["client_id", this.consumerKey],
             ["client_secret", this.consumerSecret],
             ["grant_type", aType],
@@ -205,25 +204,23 @@ OAuth2.prototype = {
           postData: params,
           onLoad: this.onAccessTokenReceived.bind(this),
           onError: this.onAccessTokenFailed.bind(this)
         }
         httpRequest(this.tokenURI, options);
     },
 
     onAccessTokenFailed: function onAccessTokenFailed(aError, aData) {
-        gConnecting[this.authURI] = false;
         if (aError != "offline") {
             this.refreshToken = null;
         }
         this.connectFailureCallback(aData);
     },
 
     onAccessTokenReceived: function onRequestTokenReceived(aData) {
-        gConnecting[this.authURI] = false;
         let result = JSON.parse(aData);
 
         this.accessToken = result.access_token;
         if ("refresh_token" in result) {
             this.refreshToken = result.refresh_token;
         }
         if ("expires_in" in result) {
             this.tokenExpires = (new Date()).getTime() + (result.expires_in * 1000);
new file mode 100644
--- /dev/null
+++ b/mailnews/base/util/OAuth2Providers.jsm
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Details of supported OAuth2 Providers.
+ */
+var EXPORTED_SYMBOLS = ["OAuth2Providers"];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+// map of hostnames to [issuer, scope]
+const kHostnames = new Map([
+  ["imap.googlemail.com", ["accounts.google.com", "http://mail.google.com/"]],
+  ["smtp.googlemail.com", ["accounts.google.com", "http://mail.google.com/"]],
+]);
+
+// map of issuers to appKey, appSecret, authURI, tokenURI
+
+// For the moment, these details are hard-coded, since Google does not
+// provide dynamic client registration. Don't copy these values for your
+// own application--register it yourself. This code (and possibly even the
+// registration itself) will disappear when this is switched to dynamic
+// client registration.
+const kIssuers = new Map ([
+  ["accounts.google.com", [
+    '406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com',
+    'kSmqreRr0qwBWJgbf5Y-PjSU',
+    'https://accounts.google.com/o/oauth2/auth',
+    'https://www.googleapis.com/oauth2/v3/token'
+  ]],
+]);
+
+/**
+ *  OAuth2Providers: Methods to lookup OAuth2 parameters for supported
+ *                   email providers.
+ */
+var OAuth2Providers = {
+
+  /**
+   * Map a hostname to the relevant issuer and scope.
+   *
+   * @param aHostname  String representing the url for an imap or smtp
+   *                   server (example "imap.googlemail.com").
+   *
+   * @returns          Array with [issuer, scope] for the hostname if found,
+   *                   else undefined. issuer is a string representing the
+   *                   organization, scope is an oauth parameter describing\
+   *                   the required access level.
+   */
+  getHostnameDetails: function (aHostname) { return kHostnames.get(aHostname);},
+
+  /**
+   * Map an issuer to OAuth2 account details.
+   *
+   * @param aIssuer    The organization issuing oauth2 parameters, example
+   *                   "accounts.google.com".
+   *
+   * @return           Array containing [appKey, appSecret, authURI, tokenURI]
+   *                   where appKey and appDetails are strings representing the
+   *                   account registered for Thunderbird with the organization,
+   *                   authURI and tokenURI are url strings representing
+   *                   endpoints to access OAuth2 authentication.
+   */
+  getIssuerDetails: function (aIssuer) { return kIssuers.get(aIssuer);}
+}
--- a/mailnews/base/util/moz.build
+++ b/mailnews/base/util/moz.build
@@ -55,16 +55,17 @@ EXTRA_JS_MODULES += [
     'IOUtils.js',
     'iteratorUtils.jsm',
     'jsTreeSelection.js',
     'JXON.js',
     'mailnewsMigrator.js',
     'mailServices.js',
     'msgDBCacheManager.js',
     'OAuth2.jsm',
+    'OAuth2Providers.jsm',
     'StringBundle.js',
     'templateUtils.js',
     'traceHelper.js',
 ]
 
 LOCAL_INCLUDES += [
   '/mozilla/netwerk/base'
 ]