Bug 1529785 part 1 - Move message archiving code into a module; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Thu, 11 Apr 2019 16:03:54 +1200
changeset 26355 1b5861b5c09a
parent 26354 cbd032bfb562
child 26356 ffedede7fa10
push id15798
push usergeoff@darktrojan.net
push dateTue, 16 Apr 2019 04:15:07 +0000
treeherdercomm-central@ffedede7fa10 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1529785
Bug 1529785 part 1 - Move message archiving code into a module; r=mkmelin
mail/base/content/folderDisplay.js
mail/base/content/mail3PaneWindowCommands.js
mail/base/content/mailCommands.js
mail/base/content/mailWindow.js
mail/base/content/mailWindowOverlay.js
mail/base/content/msgMail3PaneWindow.js
mail/base/modules/MailUtils.jsm
mail/base/modules/MessageArchiver.jsm
mail/base/modules/moz.build
mail/test/mozmill/shared-modules/test-folder-display-helpers.js
--- a/mail/base/content/folderDisplay.js
+++ b/mail/base/content/folderDisplay.js
@@ -2133,17 +2133,17 @@ FolderDisplayWidget.prototype = {
         if (allEnabled !== undefined)
           return allEnabled;
       }
     }
 
     // Either we've selected a small number of messages or we just can't
     // fast-path the result; examine all the messages.
     return this.selectedMessages.every(function(msg) {
-      let identity = getIdentityForHeader(msg);
+      let identity = MailUtils.getIdentityForHeader(msg);
       return Boolean(identity && identity.archiveEnabled);
     });
   },
 
   /**
    * @return true if all the selected messages can be deleted from their
    * folders, false otherwise.
    */
--- a/mail/base/content/mail3PaneWindowCommands.js
+++ b/mail/base/content/mail3PaneWindowCommands.js
@@ -1176,17 +1176,17 @@ function IsSendUnsentMsgsEnabled(unsentM
     return unsentMsgsFolder.getTotalMessages(false) > 0;
   }
 
   // Otherwise, we don't know where we are, so use the current identity and
   // find out if we have messages or not via that.
   let identity;
   let folders = GetSelectedMsgFolders();
   if (folders.length > 0)
-    identity = getIdentityForServer(folders[0].server);
+    identity = MailUtils.getIdentityForServer(folders[0].server);
 
   if (!identity) {
     let defaultAccount = MailServices.accounts.defaultAccount;
     if (defaultAccount)
       identity = defaultAccount.defaultIdentity;
 
     if (!identity)
       return false;
--- a/mail/base/content/mailCommands.js
+++ b/mail/base/content/mailCommands.js
@@ -6,144 +6,16 @@
 /* import-globals-from commandglue.js */
 /* import-globals-from folderDisplay.js */
 /* import-globals-from mailWindow.js */
 /* import-globals-from utilityOverlay.js */
 
 var {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
 var {MailUtils} = ChromeUtils.import("resource:///modules/MailUtils.jsm");
 
-/**
- * Get the identity that most likely is the best one to use, given the hint.
- * @param identities    nsIArray<nsIMsgIdentity> of identities
- * @param optionalHint  string containing comma separated mailboxes
- * @param useDefault    If true, use the default identity of the default
- *                      account as last choice. This is useful when all
- *                      identities are passed in. Otherwise, use the first
- *                      entity in the list.
- */
-function getBestIdentity(identities, optionalHint, useDefault = false) {
-  let identityCount = identities.length;
-  if (identityCount < 1)
-    return null;
-
-  // If we have more than one identity and a hint to help us pick one.
-  if (identityCount > 1 && optionalHint) {
-    // Normalize case on the optional hint to improve our chances of
-    // finding a match.
-    optionalHint = optionalHint.toLowerCase();
-    let hints = optionalHint.toLowerCase().split(",");
-
-    for (let i = 0; i < hints.length; i++) {
-      for (let identity of fixIterator(identities,
-                                       Ci.nsIMsgIdentity)) {
-        if (!identity.email)
-          continue;
-        if (hints[i].trim() == identity.email.toLowerCase() ||
-            hints[i].includes("<" + identity.email.toLowerCase() + ">"))
-          return identity;
-      }
-    }
-  }
-
-  // Still no matches? Give up and pick the default or the first one.
-  if (useDefault) {
-    let defaultAccount = accountManager.defaultAccount;
-    if (defaultAccount && defaultAccount.defaultIdentity)
-      return defaultAccount.defaultIdentity;
-  }
-
-  return identities.queryElementAt(0, Ci.nsIMsgIdentity);
-}
-
-function getIdentityForServer(server, optionalHint) {
-  var identities = accountManager.getIdentitiesForServer(server);
-  return getBestIdentity(identities, optionalHint);
-}
-
-/**
- * Get the identity for the given header.
- * @param hdr nsIMsgHdr message header
- * @param type nsIMsgCompType compose type the identity ise used for.
- */
-function getIdentityForHeader(hdr, type) {
-  function findDeliveredToIdentityEmail() {
-    // This function reads from currentHeaderData, which is only useful if we're
-    // looking at the currently-displayed message. Otherwise, just return
-    // immediately so we don't waste time.
-    if (hdr != gMessageDisplay.displayedMessage)
-      return "";
-
-    // Get the delivered-to headers.
-    let key = "delivered-to";
-    let deliveredTos = [];
-    let index = 0;
-    let header = "";
-    while ((header = currentHeaderData[key])) {
-      deliveredTos.push(header.headerValue.toLowerCase().trim());
-      key = "delivered-to" + index++;
-    }
-
-    // Reverse the array so that the last delivered-to header will show at front.
-    deliveredTos.reverse();
-
-    for (let i = 0; i < deliveredTos.length; i++) {
-      for (let identity of fixIterator(accountManager.allIdentities,
-                                       Ci.nsIMsgIdentity)) {
-        if (!identity.email)
-          continue;
-        // If the deliver-to header contains the defined identity, that's it.
-        if (deliveredTos[i] == identity.email.toLowerCase() ||
-            deliveredTos[i].includes("<" + identity.email.toLowerCase() + ">"))
-          return identity.email;
-      }
-    }
-    return "";
-  }
-
-  let server = null;
-  let identity = null;
-  let folder = hdr.folder;
-  if (folder) {
-    server = folder.server;
-    identity = folder.customIdentity;
-    if (identity)
-      return identity;
-  }
-
-  if (!server) {
-    let accountKey = hdr.accountKey;
-    if (accountKey) {
-      let account = accountManager.getAccount(accountKey);
-      if (account)
-        server = account.incomingServer;
-    }
-  }
-
-  let hintForIdentity = "";
-  if (type == Ci.nsIMsgCompType.ReplyToList)
-    hintForIdentity = findDeliveredToIdentityEmail();
-  else if (type == Ci.nsIMsgCompType.Template ||
-           type == Ci.nsIMsgCompType.EditTemplate ||
-           type == Ci.nsIMsgCompType.EditAsNew)
-    hintForIdentity = hdr.author;
-  else
-    hintForIdentity = hdr.recipients + "," + hdr.ccList + "," +
-                      findDeliveredToIdentityEmail();
-
-  if (server)
-    identity = getIdentityForServer(server, hintForIdentity);
-
-  if (!identity)
-    identity = getBestIdentity(accountManager.allIdentities,
-                               hintForIdentity, true);
-
-  return identity;
-}
-
 function GetNextNMessages(folder) {
   if (folder) {
     var newsFolder = folder.QueryInterface(
                      Ci.nsIMsgNewsFolder);
     if (newsFolder) {
       newsFolder.getNextNMessages(msgWindow);
     }
   }
@@ -169,16 +41,50 @@ function GetMsgKeyFromURI(uri) {
  * Compose a message.
  *
  * @param type   nsIMsgCompType    Type of composition (new message, reply, draft, etc.)
  * @param format nsIMsgCompFormat  Requested format (plain text, html, default)
  * @param folder nsIMsgFolder      Folder where the original message is stored
  * @param messageArray             Array of messages to process, often only holding one element.
  */
 function ComposeMessage(type, format, folder, messageArray) {
+  function findDeliveredToIdentityEmail(hdr) {
+    // This function reads from currentHeaderData, which is only useful if we're
+    // looking at the currently-displayed message. Otherwise, just return
+    // immediately so we don't waste time.
+    if (hdr != gMessageDisplay.displayedMessage)
+      return "";
+
+    // Get the delivered-to headers.
+    let key = "delivered-to";
+    let deliveredTos = [];
+    let index = 0;
+    let header = "";
+    while ((header = currentHeaderData[key])) {
+      deliveredTos.push(header.headerValue.toLowerCase().trim());
+      key = "delivered-to" + index++;
+    }
+
+    // Reverse the array so that the last delivered-to header will show at front.
+    deliveredTos.reverse();
+
+    for (let i = 0; i < deliveredTos.length; i++) {
+      for (let identity of fixIterator(MailServices.accounts.allIdentities,
+                                       Ci.nsIMsgIdentity)) {
+        if (!identity.email)
+          continue;
+        // If the deliver-to header contains the defined identity, that's it.
+        if (deliveredTos[i] == identity.email.toLowerCase() ||
+            deliveredTos[i].includes("<" + identity.email.toLowerCase() + ">"))
+          return identity.email;
+      }
+    }
+    return "";
+  }
+
   let msgComposeType = Ci.nsIMsgCompType;
   let ignoreQuote = false;
   let msgKey;
   if (messageArray && messageArray.length == 1) {
     msgKey = GetMsgKeyFromURI(messageArray[0]);
     if (msgKey != gMessageDisplay.keyForCharsetOverride) {
       msgWindow.charsetOverride = false;
     }
@@ -231,17 +137,17 @@ function ComposeMessage(type, format, fo
       // turn this into a new post or a reply to group.
       if (!folder.isServer && server.type == "nntp" && type == msgComposeType.New) {
         type = msgComposeType.NewsPost;
         newsgroup = folder.folderURL;
       }
 
       identity = folder.customIdentity;
       if (!identity)
-        identity = getIdentityForServer(server);
+        identity = MailUtils.getIdentityForServer(server);
       // dump("identity = " + identity + "\n");
     }
   } catch (ex) {
     dump("failed to get an identity to pre-select: " + ex + "\n");
   }
 
   // dump("\nComposeMessage from XUL: " + identity + "\n");
 
@@ -288,17 +194,17 @@ function ComposeMessage(type, format, fo
         hdr = messenger.msgHdrFromURI(messageUri);
         if (FeedMessageHandler.isFeedMessage(hdr)) {
           // Do not use the header derived identity for feeds, pass on only a
           // possible server identity from above.
           openComposeWindowForRSSArticle(null, hdr, messageUri, type,
                                          format, identity, msgWindow);
         } else {
           // Replies come here.
-          let hdrIdentity = getIdentityForHeader(hdr, type);
+          let hdrIdentity = MailUtils.getIdentityForHeader(hdr, type, findDeliveredToIdentityEmail(hdr));
           if (ignoreQuote)
             type += msgComposeType.ReplyIgnoreQuote;
           MailServices.compose.OpenComposeWindow(null, hdr, messageUri, type,
                                                  format, hdrIdentity, msgWindow);
         }
       }
   }
 }
@@ -434,17 +340,17 @@ saveAsUrlListener.prototype = {
   OnStopRunningUrl(aUrl, aExitCode) {
     messenger.saveAs(this.uri, false, this.identity, null);
   },
 };
 
 function SaveAsTemplate(uri) {
   if (uri) {
     let hdr = messenger.msgHdrFromURI(uri);
-    let identity = getIdentityForHeader(hdr, Ci.nsIMsgCompType.Template);
+    let identity = MailUtils.getIdentityForHeader(hdr, Ci.nsIMsgCompType.Template);
     let templates = MailUtils.getOrCreateFolder(identity.stationeryFolder);
     if (!templates.parent) {
       templates.setFlag(Ci.nsMsgFolderFlags.Templates);
       let isAsync = templates.server.protocolInfo.foldersCreatedAsync;
       templates.createStorageIfMissing(new saveAsUrlListener(uri, identity));
       if (isAsync)
         return;
     }
--- a/mail/base/content/mailWindow.js
+++ b/mail/base/content/mailWindow.js
@@ -590,17 +590,17 @@ function getNotificationBox(aWindow) {
   }
   return null;
 }
 
 // Given the server, open the twisty and the set the selection
 // on inbox of that server.
 // prompt if offline.
 function OpenInboxForServer(server) {
-  gFolderTreeView.selectFolder(GetInboxFolder(server));
+  gFolderTreeView.selectFolder(MailUtils.getInboxFolder(server));
 
   if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail()) {
     if (server.type != "imap")
       GetMessagesForInboxOnServer(server);
   }
 }
 
 /** Update state of zoom type (text vs. full) menu item. */
--- a/mail/base/content/mailWindowOverlay.js
+++ b/mail/base/content/mailWindowOverlay.js
@@ -20,16 +20,17 @@ var {GlodaSyntheticView} = ChromeUtils.i
 var {MailConsts} = ChromeUtils.import("resource:///modules/MailConsts.jsm");
 var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
 var {MailUtils} = ChromeUtils.import("resource:///modules/MailUtils.jsm");
 var {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
 var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 var {AddonManager} = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 var {TagUtils} = ChromeUtils.import("resource:///modules/TagUtils.jsm");
+var {MessageArchiver} = ChromeUtils.import("resource:///modules/MessageArchiver.jsm");
 
 var {BrowserToolboxProcess} = ChromeUtils.import("resource://devtools/client/framework/ToolboxProcess.jsm");
 var {ScratchpadManager} = ChromeUtils.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
 var {ExtensionParent} = ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 var {ExtensionSupport} = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
 Object.defineProperty(this, "HUDService", {
   get() {
     let { devtools } = ChromeUtils.import("resource://devtools/shared/Loader.jsm");
@@ -1192,17 +1193,17 @@ function IsReplyAllEnabled() {
   let addresses = msgHdr.author + "," + msgHdr.recipients + "," + msgHdr.ccList;
 
   // If we've got any BCCed addresses (because we sent the message), add
   // them as well.
   if ("bcc" in currentHeaderData)
     addresses += currentHeaderData.bcc.headerValue;
 
   // Check to see if my email address is in the list of addresses.
-  let myIdentity = getIdentityForHeader(msgHdr);
+  let myIdentity = MailUtils.getIdentityForHeader(msgHdr);
   let myEmail = myIdentity ? myIdentity.email : null;
   // We aren't guaranteed to have an email address, so guard against that.
   let imInAddresses = myEmail && (addresses.toLowerCase().includes(
                                     myEmail.toLowerCase()));
 
   // Now, let's get the number of unique addresses.
   let uniqueAddresses = MailServices.headerParser.removeDuplicateAddresses(addresses, "");
   let emailAddresses = {};
@@ -1356,30 +1357,18 @@ function GetFirstSelectedMsgFolder() {
   try {
     var selectedFolders = GetSelectedMsgFolders();
   } catch (e) {
     logException(e);
   }
   return (selectedFolders.length > 0) ? selectedFolders[0] : null;
 }
 
-function GetInboxFolder(server) {
-  try {
-    var rootMsgFolder = server.rootMsgFolder;
-
-    // Now find the Inbox.
-    return rootMsgFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
-  } catch (ex) {
-    dump(ex + "\n");
-  }
-  return null;
-}
-
 function GetMessagesForInboxOnServer(server) {
-  var inboxFolder = GetInboxFolder(server);
+  var inboxFolder = MailUtils.getInboxFolder(server);
 
   // If the server doesn't support an inbox it could be an RSS server or some
   // other server type. Just use the root folder and the server implementation
   // can figure out what to do.
   if (!inboxFolder)
     inboxFolder = server.rootFolder;
 
   GetNewMsgs(server, inboxFolder);
@@ -1571,307 +1560,26 @@ function MsgReplyGroup(event) {
 function MsgReplyToAllMessage(event) {
   composeMsgByType(Ci.nsIMsgCompType.ReplyAll, event);
 }
 
 function MsgReplyToListMessage(event) {
   composeMsgByType(Ci.nsIMsgCompType.ReplyToList, event);
 }
 
-// Message Archive function
-
-function BatchMessageMover() {
-  this._batches = {};
-  this._currentKey = null;
-  this._dstFolderParent = null;
-  this._dstFolderName = null;
-}
-
-BatchMessageMover.prototype = {
-
-  archiveMessages(aMsgHdrs) {
-    if (!aMsgHdrs.length)
-      return;
-
-    gFolderDisplay.hintMassMoveStarting();
-    for (let i = 0; i < aMsgHdrs.length; i++) {
-      let msgHdr = aMsgHdrs[i];
-
-      let server = msgHdr.folder.server;
-
-      // Convert date to JS date object.
-      let msgDate = new Date(msgHdr.date / 1000);
-      let msgYear = msgDate.getFullYear().toString();
-      let monthFolderName = msgYear + "-" + (msgDate.getMonth() + 1).toString().padStart(2, "0");
-
-      let archiveFolderURI;
-      let archiveGranularity;
-      let archiveKeepFolderStructure;
-
-      let identity = getIdentityForHeader(msgHdr);
-      if (!identity || FeedMessageHandler.isFeedFolder(msgHdr.folder)) {
-        // If no identity, or a server (RSS) which doesn't have an identity
-        // and doesn't want the default unrelated identity value, figure
-        // this out based on the default identity prefs.
-        let enabled = Services.prefs.getBoolPref(
-          "mail.identity.default.archive_enabled"
-        );
-        if (!enabled)
-          continue;
-
-        archiveFolderURI = server.serverURI + "/Archives";
-        archiveGranularity = Services.prefs.getIntPref(
-          "mail.identity.default.archive_granularity"
-        );
-        archiveKeepFolderStructure = Services.prefs.getBoolPref(
-          "mail.identity.default.archive_keep_folder_structure"
-        );
-      } else {
-        if (!identity.archiveEnabled)
-          continue;
-
-        archiveFolderURI = identity.archiveFolder;
-        archiveGranularity = identity.archiveGranularity;
-        archiveKeepFolderStructure = identity.archiveKeepFolderStructure;
-      }
-
-      let copyBatchKey = msgHdr.folder.URI;
-      if (archiveGranularity >= Ci.nsIMsgIdentity
-                                  .perYearArchiveFolders)
-        copyBatchKey += "\0" + msgYear;
-
-      if (archiveGranularity >= Ci.nsIMsgIdentity
-                                  .perMonthArchiveFolders)
-        copyBatchKey += "\0" + monthFolderName;
-
-      if (archiveKeepFolderStructure)
-        copyBatchKey += msgHdr.folder.URI;
-
-      // Add a key to copyBatchKey
-      if (!(copyBatchKey in this._batches)) {
-        this._batches[copyBatchKey] = {
-          srcFolder: msgHdr.folder,
-          archiveFolderURI,
-          granularity: archiveGranularity,
-          keepFolderStructure: archiveKeepFolderStructure,
-          yearFolderName: msgYear,
-          monthFolderName,
-          messages: [],
-        };
-      }
-      this._batches[copyBatchKey].messages.push(msgHdr);
-    }
-    MailServices.mfn.addListener(this, MailServices.mfn.folderAdded);
-
-    // Now we launch the code iterating over all message copies, one in turn.
-    this.processNextBatch();
-  },
-
-  processNextBatch() {
-    // get the first defined key and value
-    for (let key in this._batches) {
-      this._currentBatch = this._batches[key];
-      delete this._batches[key];
-      this.filterBatch();
-      return;
-    }
-    this._batches = null;
-    gFolderDisplay.hintMassMoveCompleted();
-    MailServices.mfn.removeListener(this);
-    // all done
-  },
-
-  filterBatch() {
-    let batch = this._currentBatch;
-
-    let filterArray = Cc["@mozilla.org/array;1"]
-                        .createInstance(Ci.nsIMutableArray);
-    for (let message of batch.messages) {
-      filterArray.appendElement(message);
-    }
-
-    // Apply filters to this batch.
-    MailServices.filters.applyFilters(
-      Ci.nsMsgFilterType.Archive,
-      filterArray, batch.srcFolder, msgWindow, this);
-    // continues with onStopOperation
-  },
-
-  onStopOperation(aResult) {
-    if (!Components.isSuccessCode(aResult)) {
-      Cu.reportError("Archive filter failed: " + aResult);
-      // We don't want to effectively disable archiving because a filter
-      // failed, so we'll continue after reporting the error.
-    }
-    // Now do the default archive processing
-    this.continueBatch();
-  },
-
-  // continue processing of default archive operations
-  continueBatch() {
-      let batch = this._currentBatch;
-      let srcFolder = batch.srcFolder;
-      let archiveFolderURI = batch.archiveFolderURI;
-      let archiveFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
-      let dstFolder = archiveFolder;
-
-      let moveArray = Cc["@mozilla.org/array;1"]
-                        .createInstance(Ci.nsIMutableArray);
-      // Don't move any items that the filter moves or deleted
-      for (let item of batch.messages) {
-        if (srcFolder.msgDatabase.ContainsKey(item.messageKey) &&
-            !(srcFolder.getProcessingFlags(item.messageKey) &
-              Ci.nsMsgProcessingFlags.FilterToMove)) {
-          moveArray.appendElement(item);
-        }
-      }
-
-      if (moveArray.length == 0)
-        this.processNextBatch(); // continue processing
-
-      // For folders on some servers (e.g. IMAP), we need to create the
-      // sub-folders asynchronously, so we chain the urls using the listener
-      // called back from createStorageIfMissing. For local,
-      // createStorageIfMissing is synchronous.
-      let isAsync = archiveFolder.server.protocolInfo.foldersCreatedAsync;
-      if (!archiveFolder.parent) {
-        archiveFolder.setFlag(Ci.nsMsgFolderFlags.Archive);
-        archiveFolder.createStorageIfMissing(this);
-        if (isAsync)
-          return; // continues with OnStopRunningUrl
-      }
-
-      let granularity = batch.granularity;
-      let forceSingle = !archiveFolder.canCreateSubfolders;
-      if (!forceSingle && (archiveFolder.server instanceof
-                           Ci.nsIImapIncomingServer))
-        forceSingle = archiveFolder.server.isGMailServer;
-      if (forceSingle)
-         granularity = Ci.nsIMsgIncomingServer.singleArchiveFolder;
-
-      if (granularity >= Ci.nsIMsgIdentity.perYearArchiveFolders) {
-        archiveFolderURI += "/" + batch.yearFolderName;
-        dstFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
-        if (!dstFolder.parent) {
-          dstFolder.createStorageIfMissing(this);
-          if (isAsync)
-            return; // continues with OnStopRunningUrl
-        }
-      }
-      if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders) {
-        archiveFolderURI += "/" + batch.monthFolderName;
-        dstFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
-        if (!dstFolder.parent) {
-          dstFolder.createStorageIfMissing(this);
-          if (isAsync)
-            return; // continues with OnStopRunningUrl
-        }
-      }
-
-      // Create the folder structure in Archives.
-      // For imap folders, we need to create the sub-folders asynchronously,
-      // so we chain the actions using the listener called back from
-      // createSubfolder. For local, createSubfolder is synchronous.
-      if (archiveFolder.canCreateSubfolders && batch.keepFolderStructure) {
-        // Collect in-order list of folders of source folder structure,
-        // excluding top-level INBOX folder
-        let folderNames = [];
-        let rootFolder = srcFolder.server.rootFolder;
-        let inboxFolder = GetInboxFolder(srcFolder.server);
-        let folder = srcFolder;
-        while (folder != rootFolder && folder != inboxFolder) {
-          folderNames.unshift(folder.name);
-          folder = folder.parent;
-        }
-        // Determine Archive folder structure.
-        for (let i = 0; i < folderNames.length; ++i) {
-          let folderName = folderNames[i];
-          if (!dstFolder.containsChildNamed(folderName)) {
-            // Create Archive sub-folder (IMAP: async).
-            if (isAsync) {
-              this._dstFolderParent = dstFolder;
-              this._dstFolderName = folderName;
-            }
-            dstFolder.createSubfolder(folderName, msgWindow);
-            if (isAsync)
-              return; // continues with folderAdded
-          }
-          dstFolder = dstFolder.getChildNamed(folderName);
-        }
-      }
-
-      if (dstFolder != srcFolder) {
-        // If the source folder doesn't support deleting messages, we
-        // make archive a copy, not a move.
-        MailServices.copy.CopyMessages(
-          srcFolder, moveArray, dstFolder,
-          srcFolder.canDeleteMessages, this, msgWindow, true
-        );
-        return; // continues with OnStopCopy
-      }
-      this.processNextBatch(); // next batch
-    },
-
-  // @implements {nsIUrlListener}
-  OnStartRunningUrl(url) {},
-  OnStopRunningUrl(url, exitCode) {
-    // this will always be a create folder url, afaik.
-    if (Components.isSuccessCode(exitCode)) {
-      this.continueBatch();
-    } else {
-      Cu.reportError("Archive failed to create folder: " + exitCode);
-      this._batches = null;
-      this.processNextBatch(); // for cleanup and exit
-    }
-  },
-
-  // also implements nsIMsgCopyServiceListener, but we only care
-  // about the OnStopCopy
-  // @implements {nsIMsgCopyServiceListener}
-  OnStartCopy() {},
-  OnProgress(aProgress, aProgressMax) {},
-  SetMessageKey(aKey) {},
-  GetMessageId() {},
-  OnStopCopy(aStatus) {
-    if (Components.isSuccessCode(aStatus)) {
-      this.processNextBatch();
-    } else {
-      // stop on error
-      Cu.reportError("Archive failed to copy: " + aStatus);
-      this._batches = null;
-      this.processNextBatch(); // for cleanup and exit
-    }
-  },
-
-  // This also implements nsIMsgFolderListener, but we only care about the
-  // folderAdded (createSubfolder callback).
-  // @implements {nsIMsgFolderListener}
-  folderAdded(aFolder) {
-    // Check that this is the folder we're interested in.
-    if (aFolder.parent == this._dstFolderParent &&
-        aFolder.name == this._dstFolderName) {
-      this._dstFolderParent = null;
-      this._dstFolderName = null;
-      this.continueBatch();
-    }
-  },
-
-  QueryInterface: ChromeUtils.generateQI(["nsIUrlListener",
-                                          "nsIMsgCopyServiceListener",
-                                          "nsIMsgOperationListener"]),
-};
-
 /**
  * Archives the selected messages
  *
  * @param event the event that caused us to call this function
  */
 function MsgArchiveSelectedMessages(event) {
-  let batchMover = new BatchMessageMover();
-  batchMover.archiveMessages(gFolderDisplay.selectedMessages);
+  let archiver = new MessageArchiver();
+  archiver.folderDisplay = gFolderDisplay;
+  archiver.msgWindow = msgWindow;
+  archiver.archiveMessages(gFolderDisplay.selectedMessages);
 }
 
 function MsgForwardMessage(event) {
   var forwardType = Services.prefs.getIntPref("mail.forward_message_mode", 0);
 
   // mail.forward_message_mode could be 1, if the user migrated from 4.x
   // 1 (forward as quoted) is obsolete, so we treat is as forward inline
   // since that is more like forward as quoted then forward as attachment
--- a/mail/base/content/msgMail3PaneWindow.js
+++ b/mail/base/content/msgMail3PaneWindow.js
@@ -1236,17 +1236,17 @@ function MigrateFolderViews() {
   var folderViewsVersion = Services.prefs.getIntPref("mail.folder.views.version");
   if (!folderViewsVersion) {
      var servers = accountManager.allServers;
      var server;
      var inbox;
      for (var index = 0; index < servers.length; index++) {
        server = servers.queryElementAt(index, Ci.nsIMsgIncomingServer);
        if (server) {
-         inbox = GetInboxFolder(server);
+         inbox = MailUtils.getInboxFolder(server);
          if (inbox)
            inbox.setFlag(Ci.nsMsgFolderFlags.Favorite);
        }
      }
     Services.prefs.setIntPref("mail.folder.views.version", 1);
   }
 }
 
--- a/mail/base/modules/MailUtils.jsm
+++ b/mail/base/modules/MailUtils.jsm
@@ -359,9 +359,113 @@ var MailUtils = {
           aCallback(false);
       }
     }
     // make sure there is at least 100 ms of not us between doing things.
     timer.initWithCallback(folder_string_setter_driver,
                            this.INTER_FOLDER_PROCESSING_DELAY_MS,
                            Ci.nsITimer.TYPE_REPEATING_SLACK);
   },
+
+  /**
+   * Get the identity that most likely is the best one to use, given the hint.
+   * @param identities    nsIArray<nsIMsgIdentity> of identities
+   * @param optionalHint  string containing comma separated mailboxes
+   * @param useDefault    If true, use the default identity of the default
+   *                      account as last choice. This is useful when all
+   *                      identities are passed in. Otherwise, use the first
+   *                      entity in the list.
+   */
+  getBestIdentity(identities, optionalHint, useDefault = false) {
+    let identityCount = identities.length;
+    if (identityCount < 1)
+      return null;
+
+    // If we have more than one identity and a hint to help us pick one.
+    if (identityCount > 1 && optionalHint) {
+      // Normalize case on the optional hint to improve our chances of
+      // finding a match.
+      optionalHint = optionalHint.toLowerCase();
+      let hints = optionalHint.toLowerCase().split(",");
+
+      for (let i = 0; i < hints.length; i++) {
+        for (let identity of fixIterator(identities,
+                                         Ci.nsIMsgIdentity)) {
+          if (!identity.email)
+            continue;
+          if (hints[i].trim() == identity.email.toLowerCase() ||
+              hints[i].includes("<" + identity.email.toLowerCase() + ">"))
+            return identity;
+        }
+      }
+    }
+
+    // Still no matches? Give up and pick the default or the first one.
+    if (useDefault) {
+      let defaultAccount = MailServices.accounts.defaultAccount;
+      if (defaultAccount && defaultAccount.defaultIdentity)
+        return defaultAccount.defaultIdentity;
+    }
+
+    return identities.queryElementAt(0, Ci.nsIMsgIdentity);
+  },
+
+  getIdentityForServer(server, optionalHint) {
+    var identities = MailServices.accounts.getIdentitiesForServer(server);
+    return this.getBestIdentity(identities, optionalHint);
+  },
+
+  /**
+   * Get the identity for the given header.
+   * @param hdr nsIMsgHdr message header
+   * @param type nsIMsgCompType compose type the identity is used for.
+   */
+  getIdentityForHeader(hdr, type, hint = "") {
+    let server = null;
+    let identity = null;
+    let folder = hdr.folder;
+    if (folder) {
+      server = folder.server;
+      identity = folder.customIdentity;
+      if (identity)
+        return identity;
+    }
+
+    if (!server) {
+      let accountKey = hdr.accountKey;
+      if (accountKey) {
+        let account = MailServices.accounts.getAccount(accountKey);
+        if (account)
+          server = account.incomingServer;
+      }
+    }
+
+    let hintForIdentity = "";
+    if (type == Ci.nsIMsgCompType.ReplyToList)
+      hintForIdentity = hint;
+    else if (type == Ci.nsIMsgCompType.Template ||
+             type == Ci.nsIMsgCompType.EditTemplate ||
+             type == Ci.nsIMsgCompType.EditAsNew)
+      hintForIdentity = hdr.author;
+    else
+      hintForIdentity = hdr.recipients + "," + hdr.ccList + "," + hint;
+
+    if (server)
+      identity = this.getIdentityForServer(server, hintForIdentity);
+
+    if (!identity)
+      identity = this.getBestIdentity(MailServices.accounts.allIdentities,
+                                      hintForIdentity, true);
+    return identity;
+  },
+
+  getInboxFolder(server) {
+    try {
+      var rootMsgFolder = server.rootMsgFolder;
+
+      // Now find the Inbox.
+      return rootMsgFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+    } catch (ex) {
+      dump(ex + "\n");
+    }
+    return null;
+  },
 };
new file mode 100644
--- /dev/null
+++ b/mail/base/modules/MessageArchiver.jsm
@@ -0,0 +1,300 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["MessageArchiver"];
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+function MessageArchiver() {
+  this._batches = {};
+  this._currentKey = null;
+  this._dstFolderParent = null;
+  this._dstFolderName = null;
+
+  this.folderDisplay = null;
+  this.msgWindow = null;
+  this.oncomplete = null;
+}
+
+MessageArchiver.prototype = {
+  archiveMessages(aMsgHdrs) {
+    if (!aMsgHdrs.length)
+      return;
+
+    if (this.folderDisplay) {
+      this.folderDisplay.hintMassMoveStarting();
+    }
+    for (let i = 0; i < aMsgHdrs.length; i++) {
+      let msgHdr = aMsgHdrs[i];
+
+      let server = msgHdr.folder.server;
+
+      // Convert date to JS date object.
+      let msgDate = new Date(msgHdr.date / 1000);
+      let msgYear = msgDate.getFullYear().toString();
+      let monthFolderName = msgYear + "-" + (msgDate.getMonth() + 1).toString().padStart(2, "0");
+
+      let archiveFolderURI;
+      let archiveGranularity;
+      let archiveKeepFolderStructure;
+
+      let identity = MailUtils.getIdentityForHeader(msgHdr);
+      if (!identity || msgHdr.folder.server.type == "rss") {
+        // If no identity, or a server (RSS) which doesn't have an identity
+        // and doesn't want the default unrelated identity value, figure
+        // this out based on the default identity prefs.
+        let enabled = Services.prefs.getBoolPref(
+          "mail.identity.default.archive_enabled"
+        );
+        if (!enabled)
+          continue;
+
+        archiveFolderURI = server.serverURI + "/Archives";
+        archiveGranularity = Services.prefs.getIntPref(
+          "mail.identity.default.archive_granularity"
+        );
+        archiveKeepFolderStructure = Services.prefs.getBoolPref(
+          "mail.identity.default.archive_keep_folder_structure"
+        );
+      } else {
+        if (!identity.archiveEnabled)
+          continue;
+
+        archiveFolderURI = identity.archiveFolder;
+        archiveGranularity = identity.archiveGranularity;
+        archiveKeepFolderStructure = identity.archiveKeepFolderStructure;
+      }
+
+      let copyBatchKey = msgHdr.folder.URI;
+      if (archiveGranularity >= Ci.nsIMsgIdentity
+                                  .perYearArchiveFolders)
+        copyBatchKey += "\0" + msgYear;
+
+      if (archiveGranularity >= Ci.nsIMsgIdentity
+                                  .perMonthArchiveFolders)
+        copyBatchKey += "\0" + monthFolderName;
+
+      if (archiveKeepFolderStructure)
+        copyBatchKey += msgHdr.folder.URI;
+
+      // Add a key to copyBatchKey
+      if (!(copyBatchKey in this._batches)) {
+        this._batches[copyBatchKey] = {
+          srcFolder: msgHdr.folder,
+          archiveFolderURI,
+          granularity: archiveGranularity,
+          keepFolderStructure: archiveKeepFolderStructure,
+          yearFolderName: msgYear,
+          monthFolderName,
+          messages: [],
+        };
+      }
+      this._batches[copyBatchKey].messages.push(msgHdr);
+    }
+    MailServices.mfn.addListener(this, MailServices.mfn.folderAdded);
+
+    // Now we launch the code iterating over all message copies, one in turn.
+    this.processNextBatch();
+  },
+
+  processNextBatch() {
+    // get the first defined key and value
+    for (let key in this._batches) {
+      this._currentBatch = this._batches[key];
+      delete this._batches[key];
+      this.filterBatch();
+      return;
+    }
+    this._batches = null;
+    if (this.folderDisplay) {
+      this.folderDisplay.hintMassMoveCompleted();
+    }
+    MailServices.mfn.removeListener(this);
+
+    if (typeof this.oncomplete == "function") {
+      this.oncomplete();
+    }
+  },
+
+  filterBatch() {
+    let batch = this._currentBatch;
+
+    let filterArray = Cc["@mozilla.org/array;1"]
+                        .createInstance(Ci.nsIMutableArray);
+    for (let message of batch.messages) {
+      filterArray.appendElement(message);
+    }
+
+    // Apply filters to this batch.
+    MailServices.filters.applyFilters(
+      Ci.nsMsgFilterType.Archive,
+      filterArray, batch.srcFolder, this.msgWindow, this);
+    // continues with onStopOperation
+  },
+
+  onStopOperation(aResult) {
+    if (!Components.isSuccessCode(aResult)) {
+      Cu.reportError("Archive filter failed: " + aResult);
+      // We don't want to effectively disable archiving because a filter
+      // failed, so we'll continue after reporting the error.
+    }
+    // Now do the default archive processing
+    this.continueBatch();
+  },
+
+  // continue processing of default archive operations
+  continueBatch() {
+    let batch = this._currentBatch;
+    let srcFolder = batch.srcFolder;
+    let archiveFolderURI = batch.archiveFolderURI;
+    let archiveFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
+    let dstFolder = archiveFolder;
+
+    let moveArray = Cc["@mozilla.org/array;1"]
+                      .createInstance(Ci.nsIMutableArray);
+    // Don't move any items that the filter moves or deleted
+    for (let item of batch.messages) {
+      if (srcFolder.msgDatabase.ContainsKey(item.messageKey) &&
+          !(srcFolder.getProcessingFlags(item.messageKey) &
+            Ci.nsMsgProcessingFlags.FilterToMove)) {
+        moveArray.appendElement(item);
+      }
+    }
+
+    if (moveArray.length == 0)
+      this.processNextBatch(); // continue processing
+
+    // For folders on some servers (e.g. IMAP), we need to create the
+    // sub-folders asynchronously, so we chain the urls using the listener
+    // called back from createStorageIfMissing. For local,
+    // createStorageIfMissing is synchronous.
+    let isAsync = archiveFolder.server.protocolInfo.foldersCreatedAsync;
+    if (!archiveFolder.parent) {
+      archiveFolder.setFlag(Ci.nsMsgFolderFlags.Archive);
+      archiveFolder.createStorageIfMissing(this);
+      if (isAsync)
+        return; // continues with OnStopRunningUrl
+    }
+
+    let granularity = batch.granularity;
+    let forceSingle = !archiveFolder.canCreateSubfolders;
+    if (!forceSingle && (archiveFolder.server instanceof
+                         Ci.nsIImapIncomingServer))
+      forceSingle = archiveFolder.server.isGMailServer;
+    if (forceSingle)
+       granularity = Ci.nsIMsgIncomingServer.singleArchiveFolder;
+
+    if (granularity >= Ci.nsIMsgIdentity.perYearArchiveFolders) {
+      archiveFolderURI += "/" + batch.yearFolderName;
+      dstFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
+      if (!dstFolder.parent) {
+        dstFolder.createStorageIfMissing(this);
+        if (isAsync)
+          return; // continues with OnStopRunningUrl
+      }
+    }
+    if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders) {
+      archiveFolderURI += "/" + batch.monthFolderName;
+      dstFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
+      if (!dstFolder.parent) {
+        dstFolder.createStorageIfMissing(this);
+        if (isAsync)
+          return; // continues with OnStopRunningUrl
+      }
+    }
+
+    // Create the folder structure in Archives.
+    // For imap folders, we need to create the sub-folders asynchronously,
+    // so we chain the actions using the listener called back from
+    // createSubfolder. For local, createSubfolder is synchronous.
+    if (archiveFolder.canCreateSubfolders && batch.keepFolderStructure) {
+      // Collect in-order list of folders of source folder structure,
+      // excluding top-level INBOX folder
+      let folderNames = [];
+      let rootFolder = srcFolder.server.rootFolder;
+      let inboxFolder = MailUtils.getInboxFolder(srcFolder.server);
+      let folder = srcFolder;
+      while (folder != rootFolder && folder != inboxFolder) {
+        folderNames.unshift(folder.name);
+        folder = folder.parent;
+      }
+      // Determine Archive folder structure.
+      for (let i = 0; i < folderNames.length; ++i) {
+        let folderName = folderNames[i];
+        if (!dstFolder.containsChildNamed(folderName)) {
+          // Create Archive sub-folder (IMAP: async).
+          if (isAsync) {
+            this._dstFolderParent = dstFolder;
+            this._dstFolderName = folderName;
+          }
+          dstFolder.createSubfolder(folderName, this.msgWindow);
+          if (isAsync)
+            return; // continues with folderAdded
+        }
+        dstFolder = dstFolder.getChildNamed(folderName);
+      }
+    }
+
+    if (dstFolder != srcFolder) {
+      // If the source folder doesn't support deleting messages, we
+      // make archive a copy, not a move.
+      MailServices.copy.CopyMessages(
+        srcFolder, moveArray, dstFolder,
+        srcFolder.canDeleteMessages, this, this.msgWindow, true
+      );
+      return; // continues with OnStopCopy
+    }
+    this.processNextBatch(); // next batch
+  },
+
+  // @implements {nsIUrlListener}
+  OnStartRunningUrl(url) {},
+  OnStopRunningUrl(url, exitCode) {
+    // this will always be a create folder url, afaik.
+    if (Components.isSuccessCode(exitCode)) {
+      this.continueBatch();
+    } else {
+      Cu.reportError("Archive failed to create folder: " + exitCode);
+      this._batches = null;
+      this.processNextBatch(); // for cleanup and exit
+    }
+  },
+
+  // also implements nsIMsgCopyServiceListener, but we only care
+  // about the OnStopCopy
+  // @implements {nsIMsgCopyServiceListener}
+  OnStartCopy() {},
+  OnProgress(aProgress, aProgressMax) {},
+  SetMessageKey(aKey) {},
+  GetMessageId() {},
+  OnStopCopy(aStatus) {
+    if (Components.isSuccessCode(aStatus)) {
+      this.processNextBatch();
+    } else {
+      // stop on error
+      Cu.reportError("Archive failed to copy: " + aStatus);
+      this._batches = null;
+      this.processNextBatch(); // for cleanup and exit
+    }
+  },
+
+  // This also implements nsIMsgFolderListener, but we only care about the
+  // folderAdded (createSubfolder callback).
+  // @implements {nsIMsgFolderListener}
+  folderAdded(aFolder) {
+    // Check that this is the folder we're interested in.
+    if (aFolder.parent == this._dstFolderParent &&
+        aFolder.name == this._dstFolderName) {
+      this._dstFolderParent = null;
+      this._dstFolderName = null;
+      this.continueBatch();
+    }
+  },
+
+  QueryInterface: ChromeUtils.generateQI(["nsIUrlListener",
+                                          "nsIMsgCopyServiceListener",
+                                          "nsIMsgOperationListener"]),
+};
--- a/mail/base/modules/moz.build
+++ b/mail/base/modules/moz.build
@@ -9,16 +9,17 @@ EXTRA_JS_MODULES += [
     'DisplayNameUtils.jsm',
     'ExtensionsUI.jsm',
     'GlobalPopupNotifications.jsm',
     'MailConsts.jsm',
     'MailInstrumentation.jsm',
     'MailMigrator.jsm',
     'MailUtils.jsm',
     'MailViewManager.jsm',
+    'MessageArchiver.jsm',
     'MsgHdrSyntheticView.jsm',
     'QuickFilterManager.jsm',
     'SearchSpec.jsm',
     'SessionStoreManager.jsm',
     'SummaryFrameManager.jsm',
     'TagUtils.jsm',
     'TBDistCustomizer.jsm',
     'Windows8WindowFrameColor.jsm',
--- a/mail/test/mozmill/shared-modules/test-folder-display-helpers.js
+++ b/mail/test/mozmill/shared-modules/test-folder-display-helpers.js
@@ -2133,17 +2133,19 @@ function assert_selected_and_displayed(.
 /**
  * Use the internal archiving code for archiving any given set of messages
  *
  * @param aMsgHdrs a list of message headers
  * */
 function archive_messages(aMsgHdrs) {
   plan_to_wait_for_folder_events("DeleteOrMoveMsgCompleted",
                                  "DeleteOrMoveMsgFailed");
-  let batchMover = new mc.window.BatchMessageMover();
+
+  let {MessageArchiver} = ChromeUtils.import("resource:///modules/MessageArchiver.jsm");
+  let batchMover = new MessageArchiver();
   batchMover.archiveMessages(aMsgHdrs);
   wait_for_folder_events();
 }
 
 /**
  * Check if the selected messages match the summarized messages.
  *
  * @param aSummarizedKeys An array of keys (messageKey + folder.URI) for the