Bug 1322473 - Support removing IM accounts from the UI and implement forgetPassword() and removeFiles() for imIncomingServer. ui-r=Paenglab, r=aleth,mkmelin rs,a=Ratty
authoraceman <acelists@atlas.sk>
Sat, 17 Dec 2016 18:49:00 +0100
changeset 21000 b0e38b2324c339f3e765ab8f63acabf10cbaaa73
parent 20999 7b3948ead9381fa9c7fd7b1ba0a76b06c4f7395a
child 21001 4eeb3f3d3ec60193f6c908170a202c20b649f390
push id12739
push useraleth@instantbird.org
push dateWed, 11 Jan 2017 23:06:31 +0000
treeherdercomm-central@b0e38b2324c3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersPaenglab, aleth, mkmelin, Ratty
bugs1322473
Bug 1322473 - Support removing IM accounts from the UI and implement forgetPassword() and removeFiles() for imIncomingServer. ui-r=Paenglab, r=aleth,mkmelin rs,a=Ratty
chat/components/public/imILogger.idl
chat/components/src/logger.js
mail/components/im/imIncomingServer.js
mail/locales/en-US/chrome/messenger/removeAccount.dtd
mailnews/base/prefs/content/removeAccount.js
mailnews/base/prefs/content/removeAccount.xul
suite/locales/en-US/chrome/mailnews/pref/removeAccount.dtd
--- a/chat/components/public/imILogger.idl
+++ b/chat/components/public/imILogger.idl
@@ -89,9 +89,16 @@ interface imILogger: nsISupports {
                        [optional] in boolean aGroupByDay);
 
   // Asynchronously iterates through log folders for all prpls and accounts and
   // invokes the callback on every log file. Returns a promise that resolves when
   // iteration is complete. If the callback returns a promise, iteration pauses
   // until the promise resolves. If the callback throws (or rejects), iteration
   // will stop and the returned promise will reject with the same error.
   jsval forEach(in imIProcessLogsCallback aCallback);
+
+  // Returns the folder storing all logs for aAccount.
+  AUTF8String getLogFolderPathForAccount(in imIAccount aAccount);
+
+  // Removes the folder storing all logs for aAccount.
+  // Be sure the account is disconnected before using this.
+  jsval deleteLogFolderForAccount(in imIAccount aAccount);
 };
--- a/chat/components/src/logger.js
+++ b/chat/components/src/logger.js
@@ -805,16 +805,41 @@ Logger.prototype = {
     } catch (aError) {
       Cu.reportError("Error getting similar logs for \"" +
                      aLog.path + "\":\n" + aError);
     }
     // If there was an error, this will return an EmptyEnumerator.
     return this._getEnumerator(entries, aGroupByDay);
   }),
 
+  getLogFolderPathForAccount: function(aAccount) {
+    return getLogFolderPathForAccount(aAccount);
+  },
+
+  deleteLogFolderForAccount: function(aAccount) {
+    if (!aAccount.disconnecting && !aAccount.disconnected)
+      throw new Error("Account must be disconnected first before deleting logs.");
+
+    if (aAccount.disconnecting)
+      Cu.reportError("Account is still disconnecting while we attempt to remove logs.");
+
+    let logPath = this.getLogFolderPathForAccount(aAccount);
+    // Find all operations on files inside the log folder.
+    let pendingPromises = [];
+    function checkLogFiles(promiseOperation, filePath) {
+      if (filePath.startsWith(logPath))
+        pendingPromises.push(promiseOperation);
+    }
+    gFilePromises.forEach(checkLogFiles);
+    // After all operations finish, remove the whole log folder.
+    return Promise.all(pendingPromises)
+                  .then(values => { OS.File.removeDir(logPath, { ignoreAbsent: true }); })
+                  .catch(aError => Cu.reportError("Failed to remove log folders:\n" + aError));
+  },
+
   forEach: Task.async(function* (aCallback) {
     let getAllSubdirs = Task.async(function* (aPaths, aErrorMsg) {
       let entries = [];
       for (let path of aPaths) {
         let iterator = new OS.File.DirectoryIterator(path);
         try {
           entries = entries.concat(yield iterator.nextBatch());
         } catch (aError) {
--- a/mail/components/im/imIncomingServer.js
+++ b/mail/components/im/imIncomingServer.js
@@ -41,19 +41,43 @@ imIncomingServer.prototype = {
   },
 
   clearAllValues: function() {
     Services.accounts.deleteAccount(this.imAccount.id);
     this._prefBranch.deleteBranch("");
     delete this._prefBranch;
     delete this._imAccount;
   },
+
+  // Returns the directory where the account would have its data stored.
+  // There are currently conversation logs only.
+  // It may not exist yet.
+  // This is used in account removal dialog and should return the same path
+  // that the removeFiles() function deletes.
+  get localPath() {
+    let logPath = Services.logs.getLogFolderPathForAccount(this.imAccount);
+    let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+    file.initWithPath(logPath);
+    return file;
+  },
+
+  // Removes files created by this account.
+  removeFiles: function() {
+    Services.logs.deleteLogFolderForAccount(this.imAccount);
+  },
+
   // called by nsMsgAccountManager while deleting an account:
   forgetSessionPassword: function() { },
 
+  forgetPassword: function() {
+    // Password is cleared in imAccount.remove()
+    // TODO: this may need to be implemented here as a separate function
+    // once IM accounts support changing username/hostname.
+  },
+
   // Shown in the "Remove Account" confirm prompt.
   get prettyName() {
     let protocol = this.imAccount.protocol.name || this.imAccount.protocol.id;
     return protocol + " - " + this.imAccount.name;
   },
 
   //XXX Flo: I don't think constructedPrettyName is visible in the UI
   get constructedPrettyName() { return "constructedPrettyName FIXME"; },
@@ -193,17 +217,22 @@ imIncomingServer.prototype = {
 
   get hostName() { return this._prefBranch.getCharPref("hostname"); },
   set hostName(aHostName) {
     this._prefBranch.setCharPref("hostname", aHostName);
   },
 
   writeToFolderCache: function() { },
   closeCachedConnections: function() { },
-  shutdown: function() { },
+
+  // Shutdown the server instance so at least disconnect from the server.
+  shutdown: function() {
+    this.imAccount.disconnect();
+  },
+
   setFilterList: function() { },
 
   get canBeDefaultServer() { return false; },
 
   // AccountManager.js verifies that spamSettings is non-null before
   // using the initialize method, but we can't just use a null value
   // because that would crash nsMsgPurgeService::PerformPurge which
   // only verifies the nsresult return value of the spamSettings
--- a/mail/locales/en-US/chrome/messenger/removeAccount.dtd
+++ b/mail/locales/en-US/chrome/messenger/removeAccount.dtd
@@ -5,15 +5,18 @@
 <!ENTITY dialogTitle                  "Remove Account and Data">
 <!ENTITY removeButton.label           "Remove">
 <!ENTITY removeButton.accesskey       "R">
 <!ENTITY removeAccount.label          "Remove account information">
 <!ENTITY removeAccount.accesskey      "a">
 <!ENTITY removeAccount.desc           "Removes only &brandShortName;'s knowledge of this account. Does not affect the account itself on the server.">
 <!ENTITY removeData.label             "Remove message data">
 <!ENTITY removeData.accesskey         "d">
+<!ENTITY removeDataChat.label         "Remove conversation data">
+<!ENTITY removeDataChat.accesskey     "d">
 <!ENTITY removeDataLocalAccount.desc  "Removes all messages, folders and filters associated with this account from your local disk. This does not affect some messages which may still be kept on the server. Do not choose this if you plan to archive the local data or re-use it in &brandShortName; later.">
 <!ENTITY removeDataServerAccount.desc "Removes all messages, folders and filters associated with this account from your local disk. Your messages and folders are still kept on the server.">
+<!ENTITY removeDataChatAccount.desc   "Removes all logs of conversations stored for this account on your local disk.">
 <!ENTITY showData.label               "Show data location">
 <!ENTITY showData.accesskey           "S">
 <!ENTITY progressPending              "Removing selected data…">
 <!ENTITY progressSuccess              "Removal succeeded.">
 <!ENTITY progressFailure              "Removal failed.">
--- a/mailnews/base/prefs/content/removeAccount.js
+++ b/mailnews/base/prefs/content/removeAccount.js
@@ -13,28 +13,41 @@ function onLoad(event) {
   gServer = window.arguments[0].account.incomingServer;
   gDialog = document.documentElement;
 
   let bundle = document.getElementById("bundle_removeAccount");
   let removeQuestion = bundle.getFormattedString("removeQuestion",
                                                  [gServer.prettyName]);
   document.getElementById("accountName").textContent = removeQuestion;
 
-  // Do not allow removal if localPath is outside of profile folder.
-  let profilePath = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile)
-  profilePath.normalize();
-  let localDirectory = gServer.localPath
-  localDirectory.normalize();
-  // TODO: bug 77652, decide what to do for deferred accounts.
-  // And inform the user if the account localPath is outside the profile.
-  if ((gServer.isDeferredTo ||
-      (gServer instanceof Components.interfaces.nsIPop3IncomingServer &&
-       gServer.deferredToAccount)) ||
-       !profilePath.contains(localDirectory)) {
-    document.getElementById("removeData").disabled = true;
+  // Allow to remove account data if it has a local storage.
+  let localDirectory = gServer.localPath;
+  if (localDirectory && localDirectory.exists()) {
+    localDirectory.normalize();
+
+    // Do not allow removal if localPath is outside of profile folder.
+    let profilePath = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile);
+    profilePath.normalize();
+
+    // TODO: bug 77652, decide what to do for deferred accounts.
+    // And inform the user if the account localPath is outside the profile.
+    if ((gServer.isDeferredTo ||
+        (gServer instanceof Components.interfaces.nsIPop3IncomingServer &&
+         gServer.deferredToAccount)) ||
+         !profilePath.contains(localDirectory)) {
+      document.getElementById("removeData").disabled = true;
+    }
+  } else {
+    document.getElementById("removeDataPossibility").collapsed = true;
+  }
+
+  if (gServer.type == "im") {
+    let dataCheckbox = document.getElementById("removeData");
+    dataCheckbox.label = dataCheckbox.getAttribute("labelChat");
+    dataCheckbox.accessKey = dataCheckbox.getAttribute("accesskeyChat");
   }
 
   enableRemove();
   window.sizeToContent();
 }
 
 function enableRemove() {
   gDialog.getButton("accept").disabled =
@@ -59,18 +72,21 @@ function openLocalDirectory() {
 }
 
 function showInfo() {
   let descs = document.querySelectorAll("vbox.indent");
   for (let desc of descs) {
     desc.collapsed = false;
   }
 
+  // TODO: bug 1238271, this should use showFor attributes if possible.
   if (gServer.type == "imap" || gServer.type == "nntp") {
     document.getElementById("serverAccount").collapsed = false;
+  } else if (gServer.type == "im") {
+    document.getElementById("chatAccount").collapsed = false;
   } else {
     document.getElementById("localAccount").collapsed = false;
   }
 
   window.sizeToContent();
   gDialog.getButton("disclosure").disabled = true;
   gDialog.getButton("disclosure").blur();
 }
--- a/mailnews/base/prefs/content/removeAccount.xul
+++ b/mailnews/base/prefs/content/removeAccount.xul
@@ -38,35 +38,43 @@
                 disabled="true"
                 accesskey="&removeAccount.accesskey;"
                 oncommand="enableRemove();"/>
       <vbox class="indent" collapsed="true">
         <description>
           &removeAccount.desc;
         </description>
       </vbox>
+      <vbox id="removeDataPossibility" collapsed="false">
       <checkbox id="removeData"
                 label="&removeData.label;"
+                labelChat="&removeDataChat.label;"
                 checked="false"
                 accesskey="&removeData.accesskey;"
+                accesskeyChat="&removeDataChat.accesskey;"
                 oncommand="enableRemove();"/>
       <vbox class="indent" collapsed="true">
         <description id="localAccount" collapsed="true">
           &removeDataLocalAccount.desc;
         </description>
         <description id="serverAccount" collapsed="true">
           &removeDataServerAccount.desc;
         </description>
+        <description id="chatAccount" collapsed="true">
+          &removeDataChatAccount.desc;
+        </description>
         <hbox align="center">
-          <button label="&showData.label;"
+          <button id="showLocalDirectory"
+                  label="&showData.label;"
                   accesskey="&showData.accesskey;"
                   oncommand="openLocalDirectory();"/>
           <label id="localDirectory" collapsed="true"/>
         </hbox>
       </vbox>
+      </vbox>
     </vbox>
     <vbox align="center">
       <spacer flex="1"/>
       <deck id="status">
         <vbox align="center">
           <label>&progressPending;</label>
           <progressmeter mode="undetermined"/>
         </vbox>
--- a/suite/locales/en-US/chrome/mailnews/pref/removeAccount.dtd
+++ b/suite/locales/en-US/chrome/mailnews/pref/removeAccount.dtd
@@ -5,15 +5,18 @@
 <!ENTITY dialogTitle                  "Remove Account and Data">
 <!ENTITY removeButton.label           "Remove">
 <!ENTITY removeButton.accesskey       "R">
 <!ENTITY removeAccount.label          "Remove account information">
 <!ENTITY removeAccount.accesskey      "a">
 <!ENTITY removeAccount.desc           "Removes only &brandShortName;'s knowledge of this account. Does not affect the account itself on the server.">
 <!ENTITY removeData.label             "Remove message data">
 <!ENTITY removeData.accesskey         "d">
+<!ENTITY removeDataChat.label         "Remove conversation data">
+<!ENTITY removeDataChat.accesskey     "d">
 <!ENTITY removeDataLocalAccount.desc  "Removes all messages, folders and filters associated with this account from your local disk. This does not affect some messages which may still be kept on the server. Do not choose this if you plan to archive the local data or re-use it in &brandShortName; later.">
 <!ENTITY removeDataServerAccount.desc "Removes all messages, folders and filters associated with this account from your local disk. Your messages and folders are still kept on the server.">
+<!ENTITY removeDataChatAccount.desc   "Removes all logs of conversations stored for this account on your local disk.">
 <!ENTITY showData.label               "Show data location">
 <!ENTITY showData.accesskey           "S">
 <!ENTITY progressPending              "Removing selected data…">
 <!ENTITY progressSuccess              "Removal succeeded.">
 <!ENTITY progressFailure              "Removal failed.">