Bug 1532388 - implement full support for IMAP and SMTP CLIENTID. r+a=mkmelin
authorDaniel Fraser <danielf@linuxmagic.com>
Wed, 15 Jan 2020 18:12:33 +0200
changeset 37881 880656c46eb874faa1f3a504029c7b3c64aeb3b1
parent 37880 c32e25a992e0bb7734130ead88421b2fac8ff99b
child 37882 7f0c44f9f86a89d7bee0322cdec916ab38a304fc
push id397
push userclokep@gmail.com
push dateMon, 10 Feb 2020 21:16:13 +0000
bugs1532388
Bug 1532388 - implement full support for IMAP and SMTP CLIENTID. r+a=mkmelin IMAP RFC Draft: https://tools.ietf.org/html/draft-yu-imap-client-id-01 SMTP RFC Draft: https://tools.ietf.org/html/draft-storey-smtp-client-id-05 This also updates the imapd.js server to expose a pseudo CLIENTID command, and adds a new xpcshell test for clientid.
mail/components/accountcreation/content/createInBackend.js
mailnews/base/public/nsIMsgIncomingServer.idl
mailnews/base/test/unit/test_accountMigration.js
mailnews/base/util/mailnewsMigrator.js
mailnews/base/util/nsMsgIncomingServer.cpp
mailnews/compose/public/nsISmtpServer.idl
mailnews/compose/src/nsComposeStrings.cpp
mailnews/compose/src/nsComposeStrings.h
mailnews/compose/src/nsSmtpProtocol.cpp
mailnews/compose/src/nsSmtpProtocol.h
mailnews/compose/src/nsSmtpServer.cpp
mailnews/imap/src/nsImapCore.h
mailnews/imap/src/nsImapProtocol.cpp
mailnews/imap/src/nsImapProtocol.h
mailnews/imap/src/nsImapServerResponseParser.cpp
mailnews/imap/test/unit/test_imapClientid.js
mailnews/imap/test/unit/xpcshell.ini
mailnews/mailnews.js
mailnews/test/fakeserver/imapd.js
--- a/mail/components/accountcreation/content/createInBackend.js
+++ b/mail/components/accountcreation/content/createInBackend.js
@@ -14,25 +14,68 @@ var { MailServices } = ChromeUtils.impor
 /**
  * Takes an |AccountConfig| JS object and creates that account in the
  * Thunderbird backend (which also writes it to prefs).
  *
  * @param {AccountConfig} config - The account to create
  * @return {nsIMsgAccount} - the newly created account
  */
 function createAccountInBackend(config) {
+  let uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
+    Ci.nsIUUIDGenerator
+  );
   // incoming server
   let inServer = MailServices.accounts.createIncomingServer(
     config.incoming.username,
     config.incoming.hostname,
     config.incoming.type
   );
   inServer.port = config.incoming.port;
   inServer.authMethod = config.incoming.auth;
   inServer.password = config.incoming.password;
+  // This new CLIENTID is for the outgoing server, and will be applied to the
+  // incoming only if the incoming username and hostname match the outgoing.
+  // We must generate this unconditionally because we cannot determine whether
+  // the outgoing server has clientid enabled yet or not, and we need to do it
+  // here in order to populate the incoming server if the outgoing matches.
+  let newOutgoingClientid = uuidGen
+    .generateUUID()
+    .toString()
+    .replace(/[{}]/g, "");
+  // Grab the base domain of both incoming and outgoing hostname in order to
+  // compare the two to detect if the base domain is the same.
+  let incomingBaseDomain;
+  let outgoingBaseDomain;
+  try {
+    incomingBaseDomain = Services.eTLD.getBaseDomainFromHost(
+      config.incoming.hostname
+    );
+  } catch (e) {
+    incomingBaseDomain = config.incoming.hostname;
+  }
+  try {
+    outgoingBaseDomain = Services.eTLD.getBaseDomainFromHost(
+      config.outgoing.hostname
+    );
+  } catch (e) {
+    outgoingBaseDomain = config.outgoing.hostname;
+  }
+  if (
+    config.incoming.username == config.outgoing.username &&
+    incomingBaseDomain == outgoingBaseDomain
+  ) {
+    inServer.clientid = newOutgoingClientid;
+  } else {
+    // If the username/hostname are different then generate a new CLIENTID.
+    inServer.clientid = uuidGen
+      .generateUUID()
+      .toString()
+      .replace(/[{}]/g, "");
+  }
+
   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);
   }
@@ -100,16 +143,20 @@ function createAccountInBackend(config) 
     config.outgoing.addThisServer &&
     !outServer &&
     !config.incoming.useGlobalPreferredServer
   ) {
     outServer = MailServices.smtp.createServer();
     outServer.hostname = config.outgoing.hostname;
     outServer.port = config.outgoing.port;
     outServer.authMethod = config.outgoing.auth;
+    // Populate the clientid if it is enabled for this outgoing server.
+    if (outServer.clientidEnabled) {
+      outServer.clientid = newOutgoingClientid;
+    }
     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);
       }
     }
 
--- a/mailnews/base/public/nsIMsgIncomingServer.idl
+++ b/mailnews/base/public/nsIMsgIncomingServer.idl
@@ -72,16 +72,27 @@ interface nsIMsgIncomingServer : nsISupp
 
   /**
    * protocol type, i.e. "pop3", "imap", "nntp", "none", etc
    * used to construct URLs
    */
   attribute ACString type;
 
   /**
+   * The CLIENTID to use for this server.
+   * @see https://tools.ietf.org/html/draft-yu-imap-client-id-01
+   */
+  attribute ACString clientid;
+
+  /**
+   * Whether the CLIENTID feature above is enabled.
+   */
+  attribute boolean clientidEnabled;
+
+  /**
    * The proper instance of nsIMsgProtocolInfo corresponding to this server type.
    */
   readonly attribute nsIMsgProtocolInfo protocolInfo;
 
   readonly attribute AString accountManagerChrome;
 
   /**
    * The schema for the local mail store, such as "mailbox", "imap", or "news"
--- a/mailnews/base/test/unit/test_accountMigration.js
+++ b/mailnews/base/test/unit/test_accountMigration.js
@@ -33,16 +33,28 @@ function run_test() {
   // Server2 has useSecAuth set to true, auth_login unset
   Services.prefs.setBoolPref("mail.server.server2.useSecAuth", true);
 
   Services.prefs.setCharPref(
     "mail.accountmanager.accounts",
     "account1,account2"
   );
 
+  // Set server1 and server2 username and hostname to test Clientid population.
+  Services.prefs.setCharPref("mail.server.server1.userName", "testuser1");
+  Services.prefs.setCharPref("mail.server.server2.userName", "testuser2");
+  Services.prefs.setCharPref(
+    "mail.server.server1.hostname",
+    "mail.sampledomain1.com"
+  );
+  Services.prefs.setCharPref(
+    "mail.server.server2.hostname",
+    "mail.sampledomain2.com"
+  );
+
   loadABFile("data/remoteContent", kPABData.fileName);
 
   let uriAllowed = Services.io.newURI(
     "chrome://messenger/content/email=yes@test.invalid"
   );
   let uriAllowed2 = Services.io.newURI(
     "chrome://messenger/content/email=yes2@test.invalid"
   );
@@ -72,16 +84,20 @@ function run_test() {
     Ci.nsIPrefLocalizedString,
     charset
   );
   Assert.ok(Services.prefs.prefHasUserValue("mailnews.view_default_charset"));
 
   // Now migrate the prefs.
   migrateMailnews();
 
+  // Check that server 1 and server 2 have the same clientid.
+  Assert.ok(Services.prefs.prefHasUserValue("mail.server.server1.clientid"));
+  Assert.ok(Services.prefs.prefHasUserValue("mail.server.server2.clientid"));
+
   // Check what has been set.
   Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server1.authMethod"));
   Assert.ok(Services.prefs.prefHasUserValue("mail.server.server2.authMethod"));
   Assert.equal(
     Services.prefs.getIntPref("mail.server.server2.authMethod"),
     Ci.nsMsgAuthMethod.secure
   );
 
@@ -102,49 +118,75 @@ function run_test() {
 
   Services.prefs.setCharPref("mail.smtpservers", "smtp1,smtp2");
 
   // smtp1 has nothing set.
 
   // smtp2 has useSecAuth set to true, auth_method unset
   Services.prefs.setBoolPref("mail.smtpserver.smtp2.useSecAuth", true);
 
+  // Set server1 and server2 username and hostname to test clientid population.
+  Services.prefs.setCharPref("mail.smtpserver.smtp1.username", "testuser1");
+  Services.prefs.setCharPref("mail.smtpserver.smtp2.username", "testuser2");
+  Services.prefs.setCharPref(
+    "mail.smtpserver.smtp1.hostname",
+    "mail.sampledomain1.com"
+  );
+  Services.prefs.setCharPref(
+    "mail.smtpserver.smtp2.hostname",
+    "mail.sampledomain2.com"
+  );
+
   // Migration should now have added permissions for the address that had them
   // and not for the one that didn't have them.
   Assert.ok(Services.prefs.getIntPref("mail.ab_remote_content.migrated") > 0);
   Assert.equal(testPermission(uriAllowed), Services.perms.ALLOW_ACTION);
   Assert.equal(testPermission(uriAllowed2), Services.perms.ALLOW_ACTION);
   Assert.equal(testPermission(uriDisallowed), Services.perms.UNKNOWN_ACTION);
 
   // Migration should have cleared the charset user pref values.
   Assert.ok(Services.prefs.getIntPref("mail.default_charsets.migrated") > 0);
   Assert.ok(!Services.prefs.prefHasUserValue("mailnews.send_default_charset"));
   Assert.ok(!Services.prefs.prefHasUserValue("mailnews.view_default_charset"));
 
   // Now migrate the prefs
   migrateMailnews();
 
+  // Check that smtpserver 1 and smtpserver 2 now have a clientid.
+  Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.clientid"));
+  Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.clientid"));
+
   Assert.ok(
     !Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.authMethod")
   );
   Assert.ok(
     Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.authMethod")
   );
   Assert.equal(
     Services.prefs.getIntPref("mail.smtpserver.smtp2.authMethod"),
     Ci.nsMsgAuthMethod.secure
   );
 
   // Now clear the authMethod for set for smtp2. This simulates the user
   // setting the value back to "3", i.e. Ci.nsMsgAuthMethod.passwordCleartext.
   Services.prefs.clearUserPref("mail.smtpserver.smtp2.authMethod");
 
+  // Now clear the mail.server.server1.clientid to test re-population.
+  Services.prefs.clearUserPref("mail.server.server2.clientid");
+
   // Now attempt migration again, e.g. a second load of TB
   migrateMailnews();
 
   // This time around, both of these should not be set.
   Assert.ok(
     !Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.authMethod")
   );
   Assert.ok(
     !Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.authMethod")
   );
+
+  // The server2 clientid should be the same as the smtpserver2 now since
+  // they are for the same mail.sampledomain2.com domain.
+  Assert.equal(
+    Services.prefs.getCharPref("mail.smtpserver.smtp2.clientid"),
+    Services.prefs.getCharPref("mail.server.server2.clientid")
+  );
 }
--- a/mailnews/base/util/mailnewsMigrator.js
+++ b/mailnews/base/util/mailnewsMigrator.js
@@ -19,16 +19,22 @@ var { MailServices } = ChromeUtils.impor
 );
 var kServerPrefVersion = 1;
 var kSmtpPrefVersion = 1;
 var kABRemoteContentPrefVersion = 1;
 var kDefaultCharsetsPrefVersion = 1;
 
 function migrateMailnews() {
   try {
+    MigrateProfileClientid();
+  } catch (e) {
+    logException(e);
+  }
+
+  try {
     MigrateServerAuthPref();
   } catch (e) {
     logException(e);
   }
 
   try {
     MigrateABRemoteContentSettings();
   } catch (e) {
@@ -38,16 +44,118 @@ function migrateMailnews() {
   try {
     MigrateDefaultCharsets();
   } catch (e) {
     logException(e);
   }
 }
 
 /**
+ * Creates the server specific 'CLIENTID' prefs and tries to pair up any imap
+ * services with smtp services which are using the same username and hostname.
+ */
+function MigrateProfileClientid() {
+  let uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
+    Ci.nsIUUIDGenerator
+  );
+  // Comma-separated list of all account ids.
+  let accounts = Services.prefs.getCharPref("mail.accountmanager.accounts");
+  // Comma-separated list of all smtp servers.
+  let smtpServers = Services.prefs.getCharPref("mail.smtpservers");
+  // If both accounts and smtpservers are empty then there is nothing to do.
+  if (accounts.length == 0 && smtpServers.length == 0) {
+    return;
+  }
+  // A cache to allow CLIENTIDS to be stored and shared across services that
+  // share a username and hostname.
+  let clientidCache = new Map();
+  // There may be accounts but no smtpservers so check the length before
+  // trying to split the smtp servers and iterate in the loop below.
+  if (smtpServers.length > 0) {
+    // Since the length of the smtpServers string is non-zero then we can split
+    // the string by comma and iterate each entry in the comma-separated list.
+    let smtpServerKeys = smtpServers.split(",");
+    // Now walk all smtp servers and generate any missing CLIENTIDS, caching
+    // all CLIENTIDS along the way to be reused for matching imap servers
+    // if possible.
+    for (let smtpServerKey of smtpServerKeys) {
+      let server = "mail.smtpserver." + smtpServerKey + ".";
+      if (
+        !Services.prefs.prefHasUserValue(server + "clientid") ||
+        !Services.prefs.getCharPref(server + "clientid")
+      ) {
+        // Always give outgoing servers a new unique CLIENTID.
+        let newClientid = uuidGen
+          .generateUUID()
+          .toString()
+          .replace(/[{}]/g, "");
+        Services.prefs.setCharPref(server + "clientid", newClientid);
+      }
+      // Cache all CLIENTIDs from all outgoing servers to reuse them for any
+      // incoming servers which have a matching username and hostname.
+      let username = Services.prefs.getCharPref(server + "username");
+      let hostname = Services.prefs.getCharPref(server + "hostname");
+      let combinedKey;
+      try {
+        combinedKey =
+          username + "@" + Services.eTLD.getBaseDomainFromHost(hostname);
+      } catch (e) {
+        combinedKey = username + "@" + hostname;
+      }
+      clientidCache.set(
+        combinedKey,
+        Services.prefs.getCharPref(server + "clientid")
+      );
+    }
+  }
+
+  let accountKeys = accounts.split(",");
+
+  // Now walk all imap accounts and generate any missing CLIENTIDS, reusing
+  // cached CLIENTIDS if possible.
+  for (let accountKey of accountKeys) {
+    let serverKey = Services.prefs.getCharPref(
+      "mail.account." + accountKey + ".server"
+    );
+    let server = "mail.server." + serverKey + ".";
+    // Check if this server needs the CLIENTID preference to be populated.
+    if (
+      !Services.prefs.prefHasUserValue(server + "clientid") ||
+      !Services.prefs.getCharPref(server + "clientid")
+    ) {
+      // Grab username + hostname to check if a CLIENTID is cached.
+      let username = Services.prefs.getCharPref(server + "userName");
+      let hostname = Services.prefs.getCharPref(server + "hostname");
+      let combinedKey;
+      try {
+        combinedKey =
+          username + "@" + Services.eTLD.getBaseDomainFromHost(hostname);
+      } catch (e) {
+        combinedKey = username + "@" + hostname;
+      }
+      if (!clientidCache.has(combinedKey)) {
+        // Generate a new CLIENTID if no matches were found from smtp servers.
+        let newClientid = uuidGen
+          .generateUUID()
+          .toString()
+          .replace(/[{}]/g, "");
+        Services.prefs.setCharPref(server + "clientid", newClientid);
+      } else {
+        // Otherwise if a cached CLIENTID was found for this username + hostname
+        // then we can just use the outgoing CLIENTID which was matching.
+        Services.prefs.setCharPref(
+          server + "clientid",
+          clientidCache.get(combinedKey)
+        );
+      }
+    }
+  }
+}
+
+/**
  * Migrates from pref useSecAuth to pref authMethod
  */
 function MigrateServerAuthPref() {
   try {
     // comma-separated list of all accounts.
     var accounts = Services.prefs
       .getCharPref("mail.accountmanager.accounts")
       .split(",");
--- a/mailnews/base/util/nsMsgIncomingServer.cpp
+++ b/mailnews/base/util/nsMsgIncomingServer.cpp
@@ -1110,22 +1110,24 @@ nsMsgIncomingServer::OnUserOrHostNameCha
 
   // 3. Notify any listeners for account server changes.
   nsCOMPtr<nsIMsgAccountManager> accountManager(
       mozilla::services::GetAccountManager());
 
   rv = accountManager->NotifyServerChanged(this);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // 4. Lastly, replace all occurrences of old name in the acct name with the
-  // new one.
+  // 4. Replace all occurrences of old name in the acct name with the new one.
   nsString acctName;
   rv = GetPrettyName(acctName);
   NS_ENSURE_SUCCESS(rv, rv);
 
+  // 5. Clear the clientid because the user or host have changed.
+  SetClientid(EmptyCString());
+
   // If new username contains @ then better do not update the account name.
   if (acctName.IsEmpty() || (!hostnameChanged && (atPos != kNotFound)))
     return NS_OK;
 
   atPos = acctName.FindChar('@');
 
   // get previous username and hostname
   nsCString userName, hostName;
@@ -1549,16 +1551,18 @@ nsMsgIncomingServer::GetIsSecure(bool *a
   return NS_OK;
 }
 
 // use the convenience macros to implement the accessors
 NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Username, "userName")
 NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, AuthMethod, "authMethod")
 NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, BiffMinutes, "check_time")
 NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Type, "type")
+NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Clientid, "clientid")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, ClientidEnabled, "clientidEnabled")
 NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, DownloadOnBiff, "download_on_biff")
 NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Valid, "valid")
 NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, EmptyTrashOnExit,
                         "empty_trash_on_exit")
 NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanDelete, "canDelete")
 NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LoginAtStartUp, "login_at_startup")
 NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer,
                         DefaultCopiesAndFoldersPrefsToServer,
--- a/mailnews/compose/public/nsISmtpServer.idl
+++ b/mailnews/compose/public/nsISmtpServer.idl
@@ -32,16 +32,27 @@ interface nsISmtpServer : nsISupports {
 
   /// The server's port.
   attribute int32_t port;
 
   /// The username to access the server with (if required)
   attribute ACString username;
 
   /**
+   * The CLIENTID to use for this server (if required).
+   * @see https://tools.ietf.org/html/draft-storey-smtp-client-id-05
+   */
+  attribute ACString clientid;
+
+  /**
+   * Whether the CLIENTID feature above is enabled.
+   */
+  attribute boolean clientidEnabled;
+
+  /**
    * The password to access the server with (if required).
    *
    * @note this is stored within the server instance but not within preferences.
    * It can be specified/saved here to avoid prompting the user constantly for
    * the sending password.
    */
   attribute AString password;
 
--- a/mailnews/compose/src/nsComposeStrings.cpp
+++ b/mailnews/compose/src/nsComposeStrings.cpp
@@ -90,15 +90,19 @@ const char* errorStringNameForErrorCode(
     case NS_ERROR_SMTP_AUTH_FAILURE:
       return "smtpAuthFailure";
     case NS_ERROR_SMTP_AUTH_GSSAPI:
       return "smtpAuthGssapi";
     case NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED:
       return "smtpAuthMechNotSupported";
     case NS_ERROR_ILLEGAL_LOCALPART:
       return "errorIllegalLocalPart";
+    case NS_ERROR_CLIENTID:
+      return "smtpClientid";
+    case NS_ERROR_CLIENTID_PERMISSION:
+      return "smtpClientidPermission";
     default:
       return "sendFailed";
   }
 #ifdef __GNUC__
 #  pragma GCC diagnostic pop
 #endif
 }
--- a/mailnews/compose/src/nsComposeStrings.h
+++ b/mailnews/compose/src/nsComposeStrings.h
@@ -62,13 +62,16 @@
 #define NS_ERROR_SMTP_AUTH_CHANGE_ENCRYPT_TO_PLAIN_SSL     NS_MSG_GENERATE_FAILURE(12595)
 #define NS_ERROR_SMTP_AUTH_CHANGE_PLAIN_TO_ENCRYPT         NS_MSG_GENERATE_FAILURE(12596)
 #define NS_ERROR_SMTP_AUTH_FAILURE                  NS_MSG_GENERATE_FAILURE(12597)
 #define NS_ERROR_SMTP_AUTH_GSSAPI                   NS_MSG_GENERATE_FAILURE(12598)
 #define NS_ERROR_SMTP_AUTH_MECH_NOT_SUPPORTED       NS_MSG_GENERATE_FAILURE(12599)
 
 #define NS_ERROR_ILLEGAL_LOCALPART                  NS_MSG_GENERATE_FAILURE(12601)
 
+#define NS_ERROR_CLIENTID                           NS_MSG_GENERATE_FAILURE(12610)
+#define NS_ERROR_CLIENTID_PERMISSION                NS_MSG_GENERATE_FAILURE(12611)
+
 const char* errorStringNameForErrorCode(nsresult aCode);
 
 #endif /* _nsComposeStrings_H__ */
 
 // clang-format on
--- a/mailnews/compose/src/nsSmtpProtocol.cpp
+++ b/mailnews/compose/src/nsSmtpProtocol.cpp
@@ -108,16 +108,18 @@ nsresult nsExplainErrorDetails(nsISmtpUr
     case NS_ERROR_SMTP_TEMP_SIZE_EXCEEDED:
     case NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_1:
     case NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_2:
     case NS_ERROR_SENDING_FROM_COMMAND:
     case NS_ERROR_SENDING_RCPT_COMMAND:
     case NS_ERROR_SENDING_DATA_COMMAND:
     case NS_ERROR_SENDING_MESSAGE:
     case NS_ERROR_SMTP_GREETING:
+    case NS_ERROR_CLIENTID:
+    case NS_ERROR_CLIENTID_PERMISSION:
       exitString = errorStringNameForErrorCode(aCode);
       bundle->GetStringFromName(exitString, eMsg);
       if (aCode == NS_ERROR_SMTP_PERM_SIZE_EXCEEDED_1) {
         // Convert the error message argument back to integer since the error
         // message string smtpPermSizeExceeded1 contains a %d.
         // (The special case can be removed if that string ever changes, then
         // %d should be changed to %S.)
         nsTextFormatter::ssprintf(msg, eMsg.get(), atoi(arg1), arg2);
@@ -228,16 +230,17 @@ nsresult nsSmtpProtocol::Initialize(nsIU
 
   m_flags = 0;
   m_prefAuthMethods = 0;
   m_failedAuthMethods = 0;
   m_currentAuthMethod = 0;
   m_usernamePrompted = false;
   m_prefSocketType = nsMsgSocketType::trySTARTTLS;
   m_tlsInitiated = false;
+  m_clientIDInitialized = false;
 
   m_url = aURL;  // Needed in nsMsgAsyncWriteProtocol::UpdateProgress().
   m_urlErrorState = NS_ERROR_FAILURE;
 
   if (aURL) m_runningURL = do_QueryInterface(aURL);
 
   // extract out message feedback if there is any.
   nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aURL);
@@ -272,16 +275,22 @@ nsresult nsSmtpProtocol::Initialize(nsIU
   // round trip communication between the client and server
   int32_t authMethod = 0;
   nsCOMPtr<nsISmtpServer> smtpServer;
   m_runningURL->GetSmtpServer(getter_AddRefs(smtpServer));
   if (smtpServer) {
     smtpServer->GetAuthMethod(&authMethod);
     smtpServer->GetSocketType(&m_prefSocketType);
     smtpServer->GetHelloArgument(m_helloArgument);
+    bool clientidEnabled = false;
+    if (NS_SUCCEEDED(smtpServer->GetClientidEnabled(&clientidEnabled)) &&
+        clientidEnabled)
+      smtpServer->GetClientid(m_clientId);
+    else
+      m_clientId.Truncate();
 
     // Query for OAuth2 support. If the SMTP server preferences don't allow
     // for OAuth2, then don't carry around the OAuth2 module any longer
     // since we won't need it.
     mOAuth2Support = do_CreateInstance(MSGIOAUTH2MODULE_CONTRACTID);
     if (mOAuth2Support) {
       bool supportsOAuth = false;
       mOAuth2Support->InitFromSmtp(smtpServer, &supportsOAuth);
@@ -843,16 +852,20 @@ nsresult nsSmtpProtocol::SendEhloRespons
         Substring(m_responseText, startPos,
                   (endPos >= 0 ? endPos : responseLength) - startPos));
 
     MsgCompressWhitespace(responseLine);
     if (responseLine.LowerCaseEqualsLiteral("starttls")) {
       SetFlag(SMTP_EHLO_STARTTLS_ENABLED);
     } else if (responseLine.LowerCaseEqualsLiteral("dsn")) {
       SetFlag(SMTP_EHLO_DSN_ENABLED);
+    } else if (responseLine.LowerCaseEqualsLiteral("clientid")) {
+      SetFlag(SMTP_EHLO_CLIENTID_ENABLED);
+      // If we have "clientid" in the ehlo response, then TLS must be present.
+      if (m_prefSocketType == nsMsgSocketType::SSL) m_tlsEnabled = true;
     } else if (StringBeginsWith(responseLine, NS_LITERAL_CSTRING("AUTH"),
                                 nsCaseInsensitiveCStringComparator())) {
       SetFlag(SMTP_AUTH);
 
       if (responseLine.Find(NS_LITERAL_CSTRING("GSSAPI"),
                             /* ignoreCase = */ true) >= 0)
         SetFlag(SMTP_AUTH_GSSAPI_ENABLED);
 
@@ -943,16 +956,48 @@ nsresult nsSmtpProtocol::SendTLSResponse
 
   ClearFlag(SMTP_EHLO_STARTTLS_ENABLED);
   m_tlsInitiated = false;
   m_nextState = SMTP_AUTH_PROCESS_STATE;
 
   return rv;
 }
 
+nsresult nsSmtpProtocol::SendClientIDResponse() {
+  if (m_responseCode / 10 == 25) {
+    // ClientID success!
+    m_clientIDInitialized = true;
+    ClearFlag(SMTP_EHLO_CLIENTID_ENABLED);
+    m_nextState = SMTP_AUTH_PROCESS_STATE;
+    return NS_OK;
+  }
+  // ClientID failed
+  nsresult errorCode;
+  if (m_responseCode == 550) {
+    // 'You are not permitted to access this'
+    // 'Access Denied' + server response
+    errorCode = NS_ERROR_CLIENTID_PERMISSION;
+  } else {
+    if (MOZ_LOG_TEST(SMTPLogModule, mozilla::LogLevel::Error)) {
+      if (m_responseCode != 501 && m_responseCode != 503 &&
+          m_responseCode != 504 && m_responseCode / 100 != 4) {
+        // If not 501, 503, 504 or 4xx, log an error.
+        MOZ_LOG(SMTPLogModule, mozilla::LogLevel::Error,
+                ("SendClientIDResponse: Unexpected error occurred, server "
+                 "responded: %s\n",
+                 m_responseText.get()));
+      }
+    }
+    errorCode = NS_ERROR_CLIENTID;
+  }
+  nsExplainErrorDetails(m_runningURL, errorCode, m_responseText.get(), nullptr);
+  m_urlErrorState = NS_ERROR_BUT_DONT_SHOW_ALERT;
+  return NS_ERROR_SMTP_AUTH_FAILURE;
+}
+
 void nsSmtpProtocol::InitPrefAuthMethods(int32_t authMethodPrefValue) {
   // for m_prefAuthMethods, using the same flags as server capabilities.
   switch (authMethodPrefValue) {
     case nsMsgAuthMethod::none:
       m_prefAuthMethods = SMTP_AUTH_NONE_ENABLED;
       break;
     // case nsMsgAuthMethod::old -- no such thing for SMTP
     case nsMsgAuthMethod::passwordCleartext:
@@ -1088,16 +1133,29 @@ nsresult nsSmtpProtocol::ProcessAuth() {
       }
     } else if (m_prefSocketType == nsMsgSocketType::alwaysSTARTTLS) {
       m_nextState = SMTP_ERROR_DONE;
       m_urlErrorState = NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS;
       return NS_ERROR_STARTTLS_FAILED_EHLO_STARTTLS;
     }
   }
 
+  if (!m_clientIDInitialized && m_tlsEnabled && !m_clientId.IsEmpty()) {
+    if (TestFlag(SMTP_EHLO_CLIENTID_ENABLED)) {
+      buffer = "CLIENTID UUID ";
+      buffer += m_clientId;
+      buffer += CRLF;
+      status = SendData(buffer.get());
+      m_nextState = SMTP_RESPONSE;
+      m_nextStateAfterResponse = SMTP_CLIENTID_RESPONSE;
+      SetFlag(SMTP_PAUSE_FOR_READ);
+      return status;
+    }
+  }
+
   (void)ChooseAuthMethod();  // advance m_currentAuthMethod
 
   // We don't need to auth, per pref, or the server doesn't advertise AUTH,
   // so skip auth and try to send message.
   if (m_prefAuthMethods == SMTP_AUTH_NONE_ENABLED || !TestFlag(SMTP_AUTH)) {
     m_nextState = SMTP_SEND_HELO_RESPONSE;
     // fake to 250 because SendHeloResponse() tests for this
     m_responseCode = 250;
@@ -1932,16 +1990,22 @@ nsresult nsSmtpProtocol::ProcessProtocol
           status = SendHeloResponse(inputStream, length);
         break;
       case SMTP_SEND_EHLO_RESPONSE:
         if (inputStream == nullptr)
           SetFlag(SMTP_PAUSE_FOR_READ);
         else
           status = SendEhloResponse(inputStream, length);
         break;
+      case SMTP_CLIENTID_RESPONSE:
+        if (inputStream == nullptr)
+          SetFlag(SMTP_PAUSE_FOR_READ);
+        else
+          status = SendClientIDResponse();
+        break;
       case SMTP_AUTH_PROCESS_STATE:
         status = ProcessAuth();
         break;
 
       case SMTP_SEND_AUTH_GSSAPI_FIRST:
         status = AuthGSSAPIFirst();
         break;
 
--- a/mailnews/compose/src/nsSmtpProtocol.h
+++ b/mailnews/compose/src/nsSmtpProtocol.h
@@ -48,27 +48,29 @@ typedef enum _SmtpState {
   SMTP_AUTH_EXTERNAL_RESPONSE,            // 20
   SMTP_AUTH_PROCESS_STATE,                // 21
   SMTP_AUTH_CRAM_MD5_CHALLENGE_RESPONSE,  // 22
   SMTP_SEND_AUTH_GSSAPI_FIRST,            // 23
   SMTP_SEND_AUTH_GSSAPI_STEP,             // 24
   SMTP_SUSPENDED,                         // 25
   SMTP_AUTH_OAUTH2_STEP,                  // 26
   SMTP_AUTH_OAUTH2_RESPONSE,              // 27
+  SMTP_CLIENTID_RESPONSE,                 // 28
 } SmtpState;
 
 // State Flags (Note, I use the word state in terms of storing
 // state information about the connection (authentication, have we sent
 // commands, etc. I do not intend it to refer to protocol state)
 #define SMTP_PAUSE_FOR_READ 0x00000001 /* should we pause for the next read */
 #define SMTP_ESMTP_SERVER 0x00000002
 #define SMTP_EHLO_DSN_ENABLED 0x00000004
 #define SMTP_EHLO_STARTTLS_ENABLED 0x00000008
 #define SMTP_EHLO_SIZE_ENABLED 0x00000010
 #define SMTP_EHLO_8BIT_ENABLED 0x00000020
+#define SMTP_EHLO_CLIENTID_ENABLED 0x00000040
 
 // insecure mechanisms follow
 #define SMTP_AUTH_LOGIN_ENABLED 0x00000100
 #define SMTP_AUTH_PLAIN_ENABLED 0x00000200
 #define SMTP_AUTH_EXTERNAL_ENABLED 0x00000400
 // secure mechanisms follow
 #define SMTP_AUTH_GSSAPI_ENABLED 0x00000800
 #define SMTP_AUTH_DIGEST_MD5_ENABLED 0x00001000
@@ -134,26 +136,29 @@ class nsSmtpProtocol : public nsMsgAsync
   RefPtr<nsMsgLineStreamBuffer>
       m_lineStreamBuffer;  // used to efficiently extract lines from the
                            // incoming data stream
 
   nsTArray<nsCString> m_addresses;
   uint32_t m_addressesLeft;
   nsCString m_mailAddr;
   nsCString m_helloArgument;
+  nsCString m_clientId;
   int32_t m_sizelimit;
 
   // *** the following should move to the smtp server when we support
   // multiple smtp servers
   bool m_usernamePrompted;
   int32_t m_prefSocketType;
   bool m_tlsEnabled;
 
   bool m_tlsInitiated;
 
+  bool m_clientIDInitialized;
+
   bool m_sendDone;
 
   int32_t m_totalAmountRead;
   int64_t m_totalMessageSize;
 
   char *m_dataBuf;
   uint32_t m_dataBufSize;
 
@@ -193,16 +198,17 @@ class nsSmtpProtocol : public nsMsgAsync
   nsresult AuthLoginStep0();
   void AuthLoginStep0Response();
   nsresult AuthLoginStep1();
   nsresult AuthLoginStep2();
   nsresult AuthLoginResponse(nsIInputStream *stream, uint32_t length);
   nsresult AuthOAuth2Step1();
 
   nsresult SendTLSResponse();
+  nsresult SendClientIDResponse();
   nsresult SendMailResponse();
   nsresult SendRecipientResponse();
   nsresult SendDataResponse();
   void SendPostData();
   nsresult SendMessageResponse();
   nsresult ProcessAuth();
 
   ////////////////////////////////////////////////////////////////////////////////////////
--- a/mailnews/compose/src/nsSmtpServer.cpp
+++ b/mailnews/compose/src/nsSmtpServer.cpp
@@ -281,16 +281,53 @@ nsSmtpServer::SetUsername(const nsACStri
 
   // If the pref value is already empty, ClearUserPref will return
   // NS_ERROR_UNEXPECTED, so don't check the rv here.
   (void)mPrefBranch->ClearUserPref("username");
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsSmtpServer::GetClientid(nsACString &aClientid) {
+  nsresult rv;
+  rv = mPrefBranch->GetCharPref("clientid", aClientid);
+  if (NS_FAILED(rv)) {
+    rv = mDefPrefBranch->GetCharPref("clientid", aClientid);
+    if (NS_FAILED(rv)) aClientid.Truncate();
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSmtpServer::SetClientid(const nsACString &aClientid) {
+  if (!aClientid.IsEmpty())
+    return mPrefBranch->SetCharPref("clientid", aClientid);
+
+  // If the pref value is already empty, ClearUserPref will return
+  // NS_ERROR_UNEXPECTED, so don't check the rv here.
+  mPrefBranch->ClearUserPref("clientid");
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsSmtpServer::GetClientidEnabled(bool *aClientidEnabled) {
+  NS_ENSURE_ARG_POINTER(aClientidEnabled);
+  nsresult rv;
+  rv = mPrefBranch->GetBoolPref("clientidEnabled", aClientidEnabled);
+  if (NS_FAILED(rv)) {
+    rv = mDefPrefBranch->GetBoolPref("clientidEnabled", aClientidEnabled);
+    if (NS_FAILED(rv)) *aClientidEnabled = false;
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsSmtpServer::SetClientidEnabled(bool aClientidEnabled) {
+  return mPrefBranch->SetBoolPref("clientidEnabled", aClientidEnabled);
+}
+
+NS_IMETHODIMP
 nsSmtpServer::GetPassword(nsAString &aPassword) {
   if (m_password.IsEmpty() && !m_logonFailed) {
     // try to avoid prompting the user for another password. If the user has set
     // the appropriate pref, we'll use the password from an incoming server, if
     // the user has already logged onto that server.
 
     // if this is set, we'll only use this, and not the other prefs
     // user_pref("mail.smtpserver.smtp1.incomingAccount", "server1");
--- a/mailnews/imap/src/nsImapCore.h
+++ b/mailnews/imap/src/nsImapCore.h
@@ -138,16 +138,17 @@ const eIMAPCapabilityFlag kHasCompressDe
 const eIMAPCapabilityFlag kHasAuthExternalCapability =    0x20000000;  /* RFC 2222 SASL AUTH EXTERNAL */
 const eIMAPCapabilityFlag kHasMoveCapability =            0x40000000;  /* Proposed MOVE RFC */
 const eIMAPCapabilityFlag kHasHighestModSeqCapability =   0x80000000;  /* Subset of RFC 3551 */
 // above are 32bit; below start the uint64_t bits 33-64
 const eIMAPCapabilityFlag kHasListExtendedCapability =    0x100000000LL;  /* RFC 5258 */
 const eIMAPCapabilityFlag kHasSpecialUseCapability =      0x200000000LL;  /* RFC 6154: Sent, Draft etc. folders */
 const eIMAPCapabilityFlag kGmailImapCapability =          0x400000000LL;  /* X-GM-EXT-1 capability extension for gmail */
 const eIMAPCapabilityFlag kHasXOAuth2Capability =         0x800000000LL;  /* AUTH XOAUTH2 extension */
+const eIMAPCapabilityFlag kHasClientIDCapability =        0x1000000000LL; /* ClientID capability */
 
 
 // this used to be part of the connection object class - maybe we should move it into
 // something similar
 typedef enum {
     kEveryThingRFC822,
     kEveryThingRFC822Peek,
     kHeadersRFC822andUid,
--- a/mailnews/imap/src/nsImapProtocol.cpp
+++ b/mailnews/imap/src/nsImapProtocol.cpp
@@ -868,16 +868,24 @@ nsresult nsImapProtocol::SetupWithUrl(ns
                               &preferPlainText);
       // If the pref has changed since the last time we ran a url,
       // clear the shell cache for this host.
       if (preferPlainText != m_preferPlainText) {
         m_hostSessionList->ClearShellCacheForHost(GetImapServerKey());
         m_preferPlainText = preferPlainText;
       }
     }
+    // If enabled, retrieve the clientid so that we can use it later.
+    bool clientidEnabled = false;
+    if (NS_SUCCEEDED(server->GetClientidEnabled(&clientidEnabled)) &&
+        clientidEnabled)
+      server->GetClientid(m_clientId);
+    else {
+      m_clientId.Truncate();
+    }
 
     bool proxyCallback = false;
     if (m_runningUrl && !m_transport /* and we don't have a transport yet */) {
       if (m_mockChannel) {
         rv = MsgExamineForProxyAsync(m_mockChannel, this,
                                      getter_AddRefs(m_proxyRequest));
         if (NS_FAILED(rv)) {
           rv = SetupWithUrlCallback(nullptr);
@@ -5553,16 +5561,30 @@ nsresult nsImapProtocol::SendDataParseIM
       break;
     }
     isResend = true;
   }
 
   return rv;
 }
 
+nsresult nsImapProtocol::ClientID() {
+  IncrementCommandTagNumber();
+  nsCString command(GetServerCommandTag());
+  command += " CLIENTID UUID ";
+  command += m_clientId;
+  command += CRLF;
+  nsresult rv = SendDataParseIMAPandCheckForNewMail(command.get(), nullptr);
+  NS_ENSURE_SUCCESS(rv, rv);
+  if (!GetServerStateParser().LastCommandSuccessful()) {
+    return NS_ERROR_FAILURE;
+  }
+  return NS_OK;
+}
+
 nsresult nsImapProtocol::AuthLogin(const char *userName,
                                    const nsString &aPassword,
                                    eIMAPCapabilityFlag flag) {
   ProgressEventFunctionUsingName("imapStatusSendingAuthLogin");
   IncrementCommandTagNumber();
 
   char *currentCommand = nullptr;
   nsresult rv;
@@ -8115,16 +8137,55 @@ bool nsImapProtocol::TryToLogon() {
         MOZ_LOG(IMAP, LogLevel::Error,
                 ("huch? there are auth methods, and we reset failed ones, but "
                  "ChooseAuthMethod still fails."));
         return false;
       }
     }
   }
 
+  // Check the uri host for localhost indicators to see if we
+  // should bypass the SSL check for clientid.
+  // Unfortunately we cannot call IsOriginPotentiallyTrustworthy
+  // here because it can only be called from the main thread.
+  bool isLocalhostConnection = false;
+  if (m_mockChannel) {
+    nsCOMPtr<nsIURI> uri;
+    m_mockChannel->GetURI(getter_AddRefs(uri));
+    if (uri) {
+      nsCString uriHost;
+      uri->GetHost(uriHost);
+      if (uriHost.Equals("127.0.0.1") || uriHost.Equals("::1") ||
+          uriHost.Equals("localhost")) {
+        isLocalhostConnection = true;
+      }
+    }
+  }
+
+  // Whether our connection can be considered 'secure' and whether
+  // we should allow the CLIENTID to be sent over this channel.
+  bool isSecureConnection =
+      (m_connectionType.EqualsLiteral("starttls") ||
+       m_connectionType.EqualsLiteral("ssl") || isLocalhostConnection);
+
+  // Before running the ClientID command we check for clientid
+  // support by checking the server capability flags for the
+  // flag kHasClientIDCapability.
+  // We check that the m_clientId string is not empty, and
+  // we ensure the connection can be considered secure.
+  if ((GetServerStateParser().GetCapabilityFlag() & kHasClientIDCapability) &&
+      !m_clientId.IsEmpty() && isSecureConnection) {
+    rv = ClientID();
+    if (NS_FAILED(rv)) {
+      MOZ_LOG(IMAP, LogLevel::Error,
+              ("TryToLogon: Could not issue CLIENTID command"));
+      skipLoop = true;
+    }
+  }
+
   // Get username, either the stored one or from user
   rv = m_imapServerSink->GetLoginUsername(userName);
   if (NS_FAILED(rv) || userName.IsEmpty()) {
     // The user hit "Cancel" on the dialog box
     skipLoop = true;
   }
 
   // clang-format off
--- a/mailnews/imap/src/nsImapProtocol.h
+++ b/mailnews/imap/src/nsImapProtocol.h
@@ -530,16 +530,17 @@ class nsImapProtocol : public nsIImapPro
   void Capability();  // query host for capabilities.
   void ID();          // send RFC 2971 app info to server
   void EnableCondStore();
   void StartCompressDeflate();
   nsresult BeginCompressing();
   void Language();  // set the language on the server if it supports it
   void Namespace();
   void InsecureLogin(const char *userName, const nsCString &password);
+  nsresult ClientID();
   nsresult AuthLogin(const char *userName, const nsString &password,
                      eIMAPCapabilityFlag flag);
   nsresult SendDataParseIMAPandCheckForNewMail(const char *data,
                                                const char *command);
   void ProcessAuthenticatedStateURL();
   void ProcessAfterAuthenticated();
   void ProcessSelectedStateURL();
   bool TryToLogon();
@@ -676,16 +677,18 @@ class nsImapProtocol : public nsIImapPro
   nsDataHashtable<nsCStringHashKey, int32_t> m_specialXListMailboxes;
 
   nsCOMPtr<nsIImapHostSessionList> m_hostSessionList;
 
   bool m_fromHeaderSeen;
 
   nsString mAcceptLanguages;
 
+  nsCString m_clientId;
+
   // progress stuff
   void SetProgressString(uint32_t aStringIndex);
 
   nsCString m_progressStringName;
   uint32_t m_stringIndex;
   int32_t m_progressCurrentNumber[IMAP_NUMBER_OF_PROGRESS_STRINGS];
   int32_t m_progressExpectedNumber;
   nsCString m_lastProgressStringName;
--- a/mailnews/imap/src/nsImapServerResponseParser.cpp
+++ b/mailnews/imap/src/nsImapServerResponseParser.cpp
@@ -1997,16 +1997,18 @@ void nsImapServerResponseParser::capabil
       else if (token.Equals("COMPRESS=DEFLATE",
                             nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasCompressDeflateCapability;
       else if (token.Equals("MOVE", nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasMoveCapability;
       else if (token.Equals("HIGHESTMODSEQ",
                             nsCaseInsensitiveCStringComparator()))
         fCapabilityFlag |= kHasHighestModSeqCapability;
+      else if (token.Equals("CLIENTID", nsCaseInsensitiveCStringComparator()))
+        fCapabilityFlag |= kHasClientIDCapability;
     }
   } while (fNextToken && endToken < 0 && !fAtEndOfLine && ContinueParse());
 
   nsImapProtocol *navCon = &fServerConnection;
   NS_ASSERTION(navCon,
                "null imap protocol connection while parsing capability "
                "response");  // we should always have this
   if (navCon) navCon->CommitCapability();
new file mode 100644
--- /dev/null
+++ b/mailnews/imap/test/unit/test_imapClientid.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import(
+  "resource:///modules/MailServices.jsm"
+);
+
+var incomingServer, server;
+
+const kUserName = "user";
+const kValidPassword = "password";
+
+var gTests = [
+  {
+    title: "Cleartext password, with server only supporting old-style login",
+    clientAuthMethod: Ci.nsMsgAuthMethod.passwordCleartext,
+    serverAuthMethods: [],
+    expectSuccess: true,
+    transaction: [
+      "capability",
+      "CLIENTID",
+      "authenticate PLAIN",
+      "list",
+      "lsub",
+    ],
+  },
+];
+
+add_task(async function() {
+  let daemon = new imapDaemon();
+  server = makeServer(daemon, "", {
+    // Make username of server match the singons.txt file
+    // (pw there is intentionally invalid)
+    kUsername: kUserName,
+    kPassword: kValidPassword,
+  });
+  server.setDebugLevel(fsDebugAll);
+  incomingServer = createLocalIMAPServer(server.port);
+
+  // Turn on CLIENTID and populate the clientid with a uuid.
+  incomingServer.clientidEnabled = true;
+  incomingServer.clientid = "4d8776ca-0251-11ea-8d71-362b9e155667";
+
+  // Connect.
+  incomingServer.performExpand(null);
+  server.performTest("LSUB");
+
+  do_check_transaction(server.playTransaction(), gTests[0].transaction, false);
+
+  server.resetTest();
+});
+
+registerCleanupFunction(function() {
+  incomingServer.closeCachedConnections();
+  server.stop();
+
+  var thread = gThreadManager.currentThread;
+  while (thread.hasPendingEvents()) {
+    thread.processNextEvent(true);
+  }
+});
--- a/mailnews/imap/test/unit/xpcshell.ini
+++ b/mailnews/imap/test/unit/xpcshell.ini
@@ -20,16 +20,17 @@ tail =
 [test_gmailAttributes.js]
 [test_gmailOfflineMsgStore.js]
 [test_imapAttachmentSaves.js]
 [test_imapAuthMethods.js]
 # Disabled per bug 553764
 skip-if = true
 [test_imapAutoSync.js]
 [test_imapChunks.js]
+[test_imapClientid.js]
 [test_imapContentLength.js]
 [test_imapCopyTimeout.js]
 [test_imapFilterActions.js]
 run-sequentially = failed once when run in parallel
 [test_imapFilterActionsPostplugin.js]
 run-sequentially = test depends on delays for async completion
 [test_imapFlagChange.js]
 [test_imapFolderCopy.js]
--- a/mailnews/mailnews.js
+++ b/mailnews/mailnews.js
@@ -448,16 +448,25 @@ pref("mail.collect_email_address_incomin
 pref("mail.collect_email_address_newsgroup", false);
 #endif
 pref("mail.collect_email_address_outgoing", true);
 // by default, use the Collected Addressbook for collection
 pref("mail.collect_addressbook", "jsaddrbook://history.sqlite");
 
 pref("mail.default_sendlater_uri", "mailbox://nobody@Local%20Folders/Unsent%20Messages");
 
+pref("mail.server.default.clientid", "");
+pref("mail.smtpserver.default.clientid", "");
+
+// This is not to be enabled by default until the prerequisite
+// changes are completed. See here for details:
+//  https://bugzilla.mozilla.org/show_bug.cgi?id=1565379
+pref("mail.server.default.clientidEnabled", false);
+pref("mail.smtpserver.default.clientidEnabled", false);
+
 pref("mail.smtpservers", "");
 pref("mail.accountmanager.accounts", "");
 
 // Last used account key value
 pref("mail.account.lastKey", 0);
 
 pref("mail.server.default.port", -1);
 pref("mail.server.default.offline_support_level", -1);
--- a/mailnews/test/fakeserver/imapd.js
+++ b/mailnews/test/fakeserver/imapd.js
@@ -722,28 +722,37 @@ function formatArg(argument, spec) {
  * rest of the line.
  */
 function IMAP_RFC3501_handler(daemon) {
   this.kUsername = "user";
   this.kPassword = "password";
   this.kAuthSchemes = []; // Added by RFC2195 extension. Test may modify as needed.
   this.kCapabilities = [
     /* "LOGINDISABLED", "STARTTLS", */
+    "CLIENTID",
   ]; // Test may modify as needed.
   this.kUidCommands = ["FETCH", "STORE", "SEARCH", "COPY"];
 
   this._daemon = daemon;
   this.closing = false;
   this.dropOnStartTLS = false;
   // map: property = auth scheme {String}, value = start function on this obj
   this._kAuthSchemeStartFunction = {};
 
   this._enabledCommands = {
     // IMAP_STATE_NOT_AUTHED
-    0: ["CAPABILITY", "NOOP", "LOGOUT", "STARTTLS", "AUTHENTICATE", "LOGIN"],
+    0: [
+      "CAPABILITY",
+      "NOOP",
+      "LOGOUT",
+      "STARTTLS",
+      "CLIENTID",
+      "AUTHENTICATE",
+      "LOGIN",
+    ],
     // IMAP_STATE_AUTHED
     1: [
       "CAPABILITY",
       "NOOP",
       "LOGOUT",
       "SELECT",
       "EXAMINE",
       "CREATE",
@@ -793,16 +802,17 @@ function IMAP_RFC3501_handler(daemon) {
   // [ ] -> optional argument.
   // x|y -> either x or y format.
   // ... -> variable args, don't parse
   this._argFormat = {
     CAPABILITY: [],
     NOOP: [],
     LOGOUT: [],
     STARTTLS: [],
+    CLIENTID: ["string", "string"],
     AUTHENTICATE: ["atom", "..."],
     LOGIN: ["string", "string"],
     SELECT: ["mailbox"],
     EXAMINE: ["mailbox"],
     CREATE: ["mailbox"],
     DELETE: ["mailbox"],
     RENAME: ["mailbox", "mailbox"],
     SUBSCRIBE: ["mailbox"],
@@ -1033,16 +1043,19 @@ IMAP_RFC3501_handler.prototype = {
   CAPABILITY(args) {
     var capa = "* CAPABILITY IMAP4rev1 " + this.kCapabilities.join(" ");
     if (this.kAuthSchemes.length > 0) {
       capa += " AUTH=" + this.kAuthSchemes.join(" AUTH=");
     }
     capa += "\0OK CAPABILITY completed";
     return capa;
   },
+  CLIENTID(args) {
+    return "OK Recognized a valid CLIENTID command, used for authentication methods";
+  },
   LOGOUT(args) {
     this.closing = true;
     if (this._selectedMailbox) {
       this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
     }
     this._state = IMAP_STATE_NOT_AUTHED;
     return "* BYE IMAP4rev1 Logging out\0OK LOGOUT completed";
   },