Bug 1491228 - refactored mbox<->maildir conversion, also fixing bug 1135309 (don't use "From -" for maildir files). r=mkmelin
☠☠ backed out by 6c36a1eb8323 ☠ ☠
authorBen Campbell <benc@thunderbird.net>
Fri, 11 Jan 2019 10:48:08 +0200
changeset 33304 67c263d7544b
parent 33303 1ff4cbc59aae
child 33305 10ad26e5d74d
push id2368
push userclokep@gmail.com
push dateMon, 28 Jan 2019 21:12:50 +0000
treeherdercomm-beta@56d23c07d815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1491228, 1135309
Bug 1491228 - refactored mbox<->maildir conversion, also fixing bug 1135309 (don't use "From -" for maildir files). r=mkmelin Solidifies interface between main thread and worker. Handles a larger variety of "From " separator lines (Bug 1491228). Is now tolerant of unquoted "From " lines in message bodies. Unit tests for various mbox forms added. The output maildir messages no longer include the "From " line (Bug 1135309).
mailnews/base/test/unit/test_mailstoreConverter.js
mailnews/base/util/converterWorker.js
mailnews/base/util/mailstoreConverter.jsm
mailnews/test/data/mbox_mboxrd
mailnews/test/data/mbox_modern
mailnews/test/data/mbox_unquoted
mailnews/test/data/readme.txt
--- a/mailnews/base/test/unit/test_mailstoreConverter.js
+++ b/mailnews/base/test/unit/test_mailstoreConverter.js
@@ -7,127 +7,105 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://testing-common/mailnews/PromiseTestUtils.jsm");
 ChromeUtils.import("resource:///modules/mailstoreConverter.jsm");
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 
 var log = Log.repository.getLogger("MailStoreConverter");
 Services.prefs.setCharPref("mail.serverDefaultStoreContractID",
                            "@mozilla.org/msgstore/berkeleystore;1");
 
-var gMsgHdrs = [];
-// {nsIMsgLocalMailFolder} folder carrying messages for the pop server.
-var gInbox;
-
-// {nsIMsgAccount} Account to convert.
-var gAccount;
-// Server for the account to convert.
-var gServer;
-
-var copyListenerWrap = {
-  SetMessageKey: function(aKey) {
-    let hdr = gInbox.GetMessageHeader(aKey);
-    gMsgHdrs.push({hdr: hdr, ID: hdr.messageId});
-  },
-  OnStopCopy: function(aStatus) {
-    // Check: message successfully copied.
-    Assert.equal(aStatus, 0);
-  }
-};
-
-var EventTarget = function () {
-  this.dispatchEvent = function(aEvent) {
-    if (aEvent.type == "progress") {
-      log.trace("Progress: " + aEvent.detail);
-    }
-  };
-};
-
-function copyFileMessage(aFile, aDestFolder, aIsDraftOrTemplate)
-{
-  let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
-  MailServices.copy.CopyFileMessage(aFile, aDestFolder, null, aIsDraftOrTemplate,
-                                    0, "", listener, null);
-  return listener.promise;
-}
-
-/**
- * Check that conversion worked for the given source.
- * @param aSource - mbox source directory
- * @param aTarget - maildir target directory
- */
-function checkConversion(aSource, aTarget) {
-  let sourceContents = aSource.directoryEntries;
-
-  while (sourceContents.hasMoreElements()) {
-    let sourceContent = sourceContents.getNext().QueryInterface(Ci.nsIFile);
-    let sourceContentName = sourceContent.leafName;
-    let ext = sourceContentName.substr(-4);
-    let targetFile = FileUtils.File(OS.Path.join(aTarget.path,sourceContentName));
-    log.debug("Checking path: " + targetFile.path);
-    if (ext == ".dat") {
-      Assert.ok(targetFile.exists());
-    } else if (sourceContent.isDirectory()) {
-      Assert.ok(targetFile.exists());
-      checkConversion(sourceContent, targetFile);
-    } else if (ext != ".msf") {
-      Assert.ok(targetFile.exists());
-      let cur = FileUtils.File(OS.Path.join(targetFile.path,"cur"));
-      Assert.ok(cur.exists());
-      let tmp = FileUtils.File(OS.Path.join(targetFile.path,"tmp"));
-      Assert.ok(tmp.exists());
-      if (targetFile.leafName == "Inbox") {
-        let curContents = cur.directoryEntries;
-        let curContentsCount = 0;
-        while (curContents.hasMoreElements()) {
-          let curContent = curContents.getNext();
-          curContentsCount++;
-        }
-        // We had 1000 msgs in the old folder. We should have that after
-        // conversion too.
-        Assert.equal(curContentsCount, 1000);
-      }
-    }
-  }
-}
-
 function run_test() {
   localAccountUtils.loadLocalMailAccount();
 
-  // {nsIMsgIncomingServer} pop server for the test.
-  gServer = MailServices.accounts.createIncomingServer("test","localhost",
-                                                       "pop3");
-  gAccount = MailServices.accounts.createAccount();
-  gAccount.incomingServer = gServer;
-  gServer.QueryInterface(Ci.nsIPop3IncomingServer);
-  gServer.valid = true;
-
-  gInbox = gAccount.incomingServer.rootFolder
-    .getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+  add_task(async function() {
+    await doMboxTest("test1", "../../../data/mbox_modern", 2);
+    await doMboxTest("test2", "../../../data/mbox_mboxrd", 2);
+    await doMboxTest("test3", "../../../data/mbox_unquoted", 2);
+    // Ideas for more tests:
+    // - check a really big mbox
+    // - check with really huge message (larger than one chunk)
+    // - check mbox with "From " line on chunk boundary
+    // - add tests for maildir->mbox conversion
+    // - check that round-trip conversion preserves messages
+    // - check that conversions preserve message body (ie that the
+    //   "From " line escaping scheme is reversable)
+  });
 
   run_next_test();
 }
 
-add_task(async function setupMessages() {
-  let msgFile = do_get_file("../../../data/bugmail10");
+/**
+ * Helper to create a server, account and inbox, and install an
+ * mbox file.
+ * @return {nsIMsgIncomingServer} a server.
+ */
+function setupServer(srvName, mboxFilename) {
+  // {nsIMsgIncomingServer} pop server for the test.
+  let server = MailServices.accounts.createIncomingServer(srvName,"localhost",
+                                                       "pop3");
+  let account= MailServices.accounts.createAccount();
+  account.incomingServer = server;
+  server.QueryInterface(Ci.nsIPop3IncomingServer);
+  server.valid = true;
+
+  let inbox = account.incomingServer.rootFolder
+    .getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
 
-  // Add 1000 messages to the "Inbox" folder.
-  for (let i = 0; i < 1000; i++) {
-    await copyFileMessage(msgFile, gInbox, false);
-  }
-});
+  // install the mbox file
+  let mboxFile = do_get_file(mboxFilename);
+  mboxFile.copyTo( inbox.filePath.parent, inbox.filePath.leafName)
+
+  // TODO: is there some way to make folder rescan the mbox?
+  // We don't need it for this, but would be nice to do things properly.
+  return server;
+}
+
 
-add_task(function testMaildirConversion() {
+/**
+ * Perform an mbox->maildir conversion test.
+ *
+ * @param {string} srvName - A unique server name to use for the test.
+ * @param {string} mboxFilename - mbox file to install and convert.
+ * @param {number} expectCnt - Number of messages expected.
+ * @return {nsIMsgIncomingServer} a server.
+ */
+async function doMboxTest(srvName, mboxFilename, expectCnt) {
+  // set up an account+server+inbox and copy in the test mbox file
+  let server = setupServer(srvName, mboxFilename);
+
   let mailstoreContractId = Services.prefs.getCharPref(
-    "mail.server." + gServer.key + ".storeContractID");
-  do_test_pending();
-  let pConverted = convertMailStoreTo(mailstoreContractId, gServer,
-                                      new EventTarget());
-  let originalRootFolder = gServer.rootFolder.filePath;
-  pConverted.then(function(aVal) {
-    log.debug("Conversion done: " + originalRootFolder.path + " => " + aVal);
-    let newRootFolder = gServer.rootFolder.filePath;
-    checkConversion(originalRootFolder, newRootFolder);
-    do_test_finished();
-  }).catch(function(aReason) {
-    log.error("Conversion Failed: " + aReason.error);
-    ok(false); // Fail the test!
-  });
-});
+    "mail.server." + server.key + ".storeContractID");
+
+  let aVal = await convertMailStoreTo(
+    mailstoreContractId, server, new EventTarget());
+  // NOTE: convertMailStoreTo() will suppress exceptions in it's
+  // worker, which makes unittest failures trickier to read...
+
+  let originalRootFolder = server.rootFolder.filePath;
+
+  // Converted. Now find resulting Inbox/cur directory so
+  // we can count the messages there.
+
+  let inbox = server.rootFolder
+    .getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+  // NOTE: the conversion updates the path of the root folder,
+  // but _not_ the path of the inbox...
+  // Ideally, we'd just use inbox.filePath here, but
+  // instead we'll have compose the path manually.
+
+  let curDir = server.rootFolder.filePath;
+  curDir.append(inbox.filePath.leafName);
+  curDir.append("cur");
+
+  // Sanity check.
+  Assert.ok(curDir.isDirectory(), "'cur' directory created" );
+
+  // Check number of messages in Inbox/cur is what we expect.
+  let cnt = 0;
+  let it = curDir.directoryEntries;
+  while (it.hasMoreElements()) {
+    let curContent = it.getNext();
+    cnt++;
+  }
+
+  Assert.equal(cnt, expectCnt, "expected number of messages (" + mboxFilename + ")");
+}
+
--- a/mailnews/base/util/converterWorker.js
+++ b/mailnews/base/util/converterWorker.js
@@ -1,314 +1,483 @@
 /* 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/. */
 
+/* eslint-env mozilla/chrome-worker, node */
+
 /**
- * This worker performs one of a set of operations requested by
- * mailstoreConverter.jsm. The possible operations are:
- * - copy a file (.dat or .msf)
- * - split a mbox file out into a maildir
- * - join the contents of a maildir into a mbox file
- * - create a subfolder (.sbd dir)
- * - handle a .mozmsgs directory
+ * This worker will perform mbox<->maildir conversions on a tree of
+ * directories. It operates purely at the filesystem level.
+ *
+ * The initial message data should pass in these params to control
+ * the conversion:
+ *
+ * srcType  - source mailstore type ('mbox' or 'maildir')
+ * destType - destination mailstore type ('maildir' or 'mbox')
+ * srcRoot  - root path of source (eg ".../ImapMail/imap.example.com")
+ * destRoot - root path of destination (eg "/tmp/imap.example.com-maildir")
+ *
+ * The conversion is non-destructive - srcRoot will be left untouched.
  *
- * The caller relies on each worker to send the right number (and type)
- * of update notifications for the operation that worker is responsible
- * for.
- * The caller counts notifications to detect when the overall mailstore
- * conversion is complete.
+ * The worker will post progess messages back to the main thread of
+ * the form:
+ *
+ *   {"msg": "progress", "val": val, "total": total}
+ *
+ * Where `val` is the current progress, out of `total`.
+ * The units used for val and total are undefined.
  *
- * Currently, the worker decides which operation it is responsible for
- * performing by looking at:
- *  - the name of the source
- *  - the type of the source (file or directory),
- *  - xpcom interface of the source mailstore (maildir or mbox).
- * Since mailstoreConverter.jsm is already scanning the store
- * and making these decisions, it would probably make sense to have it
- * specify the operation type explicitly rather than repeating the
- * logic here so the worker can decide.
+ * When the conversion is complete, before exiting, the worker sends a
+ * message of the form:
+ *
+ *   {"msg": "success"}
+ *
+ * Errors are posted back to the main thread via the standard
+ * "error" event.
+ *
  */
 
-self.importScripts("resource://gre/modules/osfile.jsm");
-self.addEventListener("message", function(e) {
+importScripts("resource://gre/modules/osfile.jsm");
+
+/**
+ * Merge all the messages in a maildir into a single mbox file.
+ *
+ * @param {String} maildir              - Path to the source maildir.
+ * @param {String} mboxFilename         - Path of the mbox file to create.
+ * @param {Function(Number)} progressFn - Function to be invoked regularly with
+ *                                        progress updates. Param is number of
+ *                                        "units" processed since last update.
+ */
+function maildirToMBox(maildir, mboxFilename, progressFn) {
+  // Helper to format dates
+  // eg "Thu Jan 18 12:34:56 2018"
+  let fmtUTC = function(d) {
+    const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+    const monthNames = ["Jan", "Feb", "Mar", "Apr",
+      "May", "Jun", "Jul", "Aug",
+      "Sep", "Oct", "Nov", "Dec"];
+    return dayNames[d.getUTCDay()] +
+      " " + monthNames[d.getUTCMonth()] +
+      " " + d.getUTCDate().toString().padStart(2) +
+      " " + d.getUTCHours().toString().padStart(2, "0") +
+      ":" + d.getUTCMinutes().toString().padStart(2, "0") +
+      ":" + d.getUTCSeconds().toString().padStart(2, "0") +
+      " " + d.getUTCFullYear();
+  };
+
+  let encoder = new TextEncoder();
+  let mboxFile = OS.File.open(mboxFilename, {write: true, create: true}, {});
+
+  // Iterate over all the message files in "cur".
+  let curPath = OS.Path.join(maildir, "cur");
+  let iterator = new OS.File.DirectoryIterator(curPath);
   try {
-    // {String} sourceFile - path to file or directory encountered.
-    var sourceFile = e.data[1];
-    // {String} dest - path to directory in which the new files or directories
-    // need to be created.
-    var dest = e.data[0];
-    // {String} destFile - name of the file or directory encountered.
-    var destFile = e.data[2];
-    var mailstoreContractId = e.data[3];
-    var tmpDir = e.data[4];
-    var serverType = e.data[5];
-    var stat = OS.File.stat(sourceFile);
+    let files = [];
+    if ("winCreationDate" in OS.File.DirectoryIterator.Entry.prototype) {
+      // Under Windows, additional information allow us to sort files immediately
+      // without having to perform additional I/O.
+      iterator.forEach(function(ent) {
+        files.push({path: ent.path, creationDate: ent.winCreationDate});
+      });
+    } else {
+      // Under other OSes, we need to call OS.File.stat
+      iterator.forEach(function(ent) {
+        files.push({path: ent.path, creationDate: OS.File.stat(ent.path).creationDate});
+      });
+    }
+    // Sort by creation time.
+    files.sort(function(a, b) {
+        return a.creationDate - b.creationDate;
+    });
+
+    for (let ent of files) {
+      let inFile = OS.File.open(ent.path);
+      try {
+        let raw = inFile.read();
+        // Old converter had a bug where maildir messages included the
+        // leading "From " marker, so we need to cope with any
+        // cases of this left in the wild.
+        if (String.fromCharCode.apply(null, raw.slice(0, 5)) != "From ") {
+          // Write the separator line.
+          // Technically, timestamp should be the reception time of the
+          // message, but we don't really want to have to parse the
+          // message here and nothing is likely to rely on iterator.
+          let sepLine = "From - " + fmtUTC(new Date()) + "\n";
+          mboxFile.write(encoder.encode(sepLine));
+        }
+
+        mboxFile.write(raw);
+      } finally {
+        inFile.close();
+      }
+      // Maildir progress is one per message.
+      progressFn(1);
+    }
+  } finally {
+    iterator.close();
+    mboxFile.close();
+  }
+}
 
-    if (stat.isDir && sourceFile.substr(-8) == ".mozmsgs") {
-      // it's an OS search integration dir.
-      // A no-op for now. Maildir/OS search integration is still
-      // a little undecided (see bug 1144478).
-      return;
-    }
+/**
+ * Split an mbox file up into a maildir.
+ *
+ * @param {String} mboxPath             - Path of the mbox file to split.
+ * @param {String} maildirPath          - Path of the maildir to create.
+ * @param {Function(Number)} progressFn - Function to be invoked regularly with
+ *                                        progress updates. One parameter is
+ *                                        passed - the number of "cost units"
+ *                                        since the previous update.
+ */
+function mboxToMaildir(mboxPath, maildirPath, progressFn) {
+  // Create the maildir structure.
+  OS.File.makeDir(maildirPath);
+  let curDirPath = OS.Path.join(maildirPath, "cur");
+  let tmpDirPath = OS.Path.join(maildirPath, "tmp");
+  OS.File.makeDir(curDirPath);
+  OS.File.makeDir(tmpDirPath);
 
-    if (stat.isDir && sourceFile.substr(-4) == ".sbd") {
-      // it's a subfolder
-      OS.File.makeDir(dest, {from: tmpDir});
-      OS.File.makeDir(OS.Path.join(dest, destFile), {from: tmpDir});
+  let decoder = new TextDecoder();
+  let encoder = new TextEncoder();
+
+  const CHUNK_SIZE = 10000000;
+  // SAFE_MARGIN is how much to keep back between chunks in order to
+  // cope with separator lines which might span chunks.
+  const SAFE_MARGIN = 100;
+
+  // A regexp to match mbox separator lines.
+  // We support lines like:
+  // "From "
+  // "From MAILER-DAEMON Fri Jul  8 12:08:34 2011"
+  // "From - Mon Jul 11 12:08:34 2011"
+  // "From bob@example.com Fri Jul  8 12:08:34 2011"
+  // we also require a message header on the next line, in order
+  // to better cope with unescaped "From " lines in the message body.
+  // note: the first subexpression matches the separator line, so
+  // it can be removed from the input.
+  let sepRE = /^((?:From \r?\n)|(?:From [\S]+ \S{3} \S{3} [ \d]\d \d\d:\d\d:\d\d \d{4}\r?\n))[\x21-\x7E]+:/gm;
 
-      // Send message to "mailstoreConverter.jsm" indicating that a directory
-      // was created.
-      // This would indicate "progress" for an imap account but not for a pop
-      // account if the number of messages in the pop account is more than 0 and
-      // mailstore type is mbox.
-      // This would indicate "progress" for a pop account if the number of
-      // messages in the pop account is 0 and the mailstore type is
-      // mbox.
-      // This would indicate "progress" for pop or imap account if the noumber
-      // of messages in the account is 0.
-      self.postMessage(["dir", sourceFile]);
-      return;
+  // Use timestamp as starting name for output messages, incrementing
+  // by one for each.
+  let ident = Date.now();
+  let outFile = null;
+
+  let writeToMsg = function(text) {
+    if (outFile === null) {
+      let outPath = OS.Path.join(curDirPath, ident.toString());
+      ident += 1;
+      outFile = OS.File.open(outPath, {write: true, create: true}, {});
+    }
+    let raw = encoder.encode(text);
+    outFile.write(raw);
+    // for mbox->maildir conversion, progress measured in bytes
+    progressFn(raw.byteLength);
+  };
+
+  let closeExistingMsg = function() {
+    if (outFile) {
+      outFile.close();
+      outFile = null;
+    }
+  };
+
+  let mboxFile = OS.File.open(mboxPath);
+  let buf = "";
+  let eof = false;
+  while (!eof) {
+    let raw = mboxFile.read(CHUNK_SIZE);
+    buf = buf + decoder.decode(raw);
+    eof = (raw.byteLength < CHUNK_SIZE);
+
+    let pos = 0;
+    sepRE.lastIndex = 0;  // start at beginning of buf
+    let m = null;
+    while ((m = sepRE.exec(buf)) !== null) {
+      // Output everything up to the line separator.
+      if (m.index > pos) {
+        writeToMsg(buf.substring(pos, m.index));
+      }
+      pos = m.index;
+      pos += m[1].length;  // skip the "From " line
+      closeExistingMsg();
     }
 
-    if (mailstoreContractId == "@mozilla.org/msgstore/maildirstore;1" &&
-        stat.isDir && sourceFile.substr(-4) != ".sbd") {
-      // copy messages from maildir -> mbox
-
-      // Create a directory with path 'dest'.
-      OS.File.makeDir(dest, {from: tmpDir});
-
-      // If the file with path 'dest/destFile' does not exist, create it,
-      // open it for writing. This is the mbox msg file with the same name as
-      // 'sourceFile'.
-      let mboxFile;
-      if (!OS.File.exists(OS.Path.join(dest,destFile))) {
-        mboxFile = OS.File.open(OS.Path.join(dest,destFile), {write: true,
-          create: true}, {});
-      }
-
-      // If length of 'e.data' is greater than 6, we know that e.data carries
-      // maildir msg file names.
-      if (e.data.length > 6) {
-        for(let msgCount = 0; msgCount < e.data.length - 6; msgCount++) {
-          let n = e.data[msgCount + 6];
-          // Open the file 'sourceFile/cur/msgFile' for reading.
-          let msgFileOpen = OS.File.open(OS.Path.join(sourceFile, "cur", n));
-          mboxFile.write(msgFileOpen.read());
-          msgFileOpen.close();
-
-          // Send a message to "mailstoreConverter.jsm" indicating that a
-          // msg was copied. This would indicate "progress" for both imap and
-          // pop accounts if mailstore type is maildir and the no. of
-          // msgs in the account is greater than zero.
-          self.postMessage(["copied", OS.Path.join(sourceFile, "cur", n)]);
-        }
+    // Deal with whatever is left in the buffer.
+    let endPos = buf.length;
+    if (!eof) {
+      // Keep back enough to cope with separator lines crossing
+      // chunk boundaries.
+      endPos -= SAFE_MARGIN;
+      if (endPos < pos) {
+        endPos = pos;
       }
-
-      mboxFile.close();
-
-      // Send a message to "mailstoreConverter.jsm" indicating that an mbox msg
-      // file was created. This would indicate "progress" for both imap and pop
-      // accounts if mailstore type is maildir and the no. of messages in
-      // the account is 0.
-      self.postMessage(["file", sourceFile, e.data.length]);
-      return;
-    }
-
-
-    // If a file is encountered, then if it is a .dat file, copy the
-    // file to the directory whose path is in 'dest'.
-    // For Local Folders, pop3, and movemail accounts, when the .msf files
-    // are copied, something goes wrong with the .msf files and the messages
-    // don't show up. Thunderbird automatically creates .msf files. So to
-    // resolve this, .msf files are not copied for Local Folders, pop3 and
-    // movemail accounts.
-    let ext = sourceFile.substr(-4);
-    if (!stat.isDir && ((ext == ".msf") || (ext == ".dat"))) {
-      if (ext == ".dat" || (serverType == "imap" || serverType == "nntp")) {
-        // If the directory with path 'dest' does not exist, create it.
-        if (!OS.File.exists(dest)) {
-          OS.File.makeDir(dest, {from: tmpDir});
-        }
-        OS.File.copy(sourceFile, OS.Path.join(dest,destFile));
-      }
-
-      // Send a message to "mailstoreConverter.jsm" indicating that a .msf or
-      // .dat file was copied.
-      // This is used to indicate progress on IMAP accounts if mailstore
-      // type is mbox.
-      // This is used to indicate progress on pop accounts if the no. of msgs
-      // in the account is 0 and mailstore type is mbox.
-      // This is used to indicate progress on pop and imap accounts if the
-      // no. of msgs in the account is 0 and mailstore type is maildir.
-      self.postMessage(["msfdat", sourceFile]);
-      return;
     }
 
-    // All other files are assumed to be mbox.
-    if (!stat.isDir && mailstoreContractId != "@mozilla.org/msgstore/maildirstore;1") {
-      // An mbox message file is encountered. Split it up into a maildir.
+    if (endPos > pos) {
+      writeToMsg(buf.substring(pos, endPos));
+    }
+    buf = buf.substring(endPos);
 
-      const constNoOfBytes = 10000000;
-      // (TODO: check this doesn't bound the size of messages we can convert!)
-
-      // Create a directory with path 'dest'.
-      OS.File.makeDir(dest, {from: tmpDir});
+  }
+  closeExistingMsg();
+}
 
-      // Create a directory with same name as the file encountered in the
-      // directory with path 'dest'.
-      // In this directory create a directory with name "cur" and a directory
-      // with name "tmp".
-      OS.File.makeDir(OS.Path.join(dest, destFile));
-      OS.File.makeDir(OS.Path.join(dest, destFile, "cur"));
-      OS.File.makeDir(OS.Path.join(dest, destFile, "tmp"));
-
-      let decoder = new TextDecoder();
-      let encoder = new TextEncoder();
+/**
+ * Check if directory is a subfolder directory.
+ *
+ * @param {String} name     - Name of directory to check.
+ * @returns {Boolean}       - true if subfolder.
+ */
+function isSBD(name) {
+  return name.substr(-4) == ".sbd";
+}
 
-      // File to which the message is to be copied.
-      let targetFile = null;
-      // Get a timestamp for file name.
-      let name = Date.now();
-      // No. of bytes to be read from the source file.
-      // Needs to be a large size to read in chunks.
-      let noOfBytes = constNoOfBytes;
-      // 'text' holds the string that was read.
-      let text = null;
-      // Index of last match in 'text'.
-      let lastMatchIndex;
-      // Current position in the source file before reading bytes from it.
-      let position;
-      // New position in the source file after reading bytes from it.
-      let nextPos;
-      // New length of the text read from source file.
-      let nextLen;
-      // Position in the file after reading the bytes in the previous
-      // iteration.
-      let prevPos = 0;
-      // Length of the text read from source file in the previous
-      // iteration.
-      let prevLen = 0;
-      // Bytes read from the source file are decoded into a string and
-      // assigned to 'textNew'.
-      let textNew;
+/**
+ * Check if file is a type which should be copied verbatim as part of a
+ * conversion.
+ *
+ * @param {String} name     - Name of file to check.
+ * @returns {Boolean}       - true if file should be copied verbatim.
+ */
+function isFileToCopy(name) {
+  let ext4 = name.substr(-4);
+  if (ext4 == ".msf" || ext4 == ".dat") {
+    return true;
+  }
+  return false;
+}
 
-      // Read the file. Since the files can be large, we read it in chunks.
-      let sourceFileOpen = OS.File.open(sourceFile);
-      while (true) {
-        position = sourceFileOpen.getPosition();
-        let array = sourceFileOpen.read(noOfBytes);
-        textNew = decoder.decode(array);
-        nextPos = sourceFileOpen.getPosition();
-        nextLen = textNew.length;
-
-        if (nextPos == prevPos && nextLen == prevLen) {
-          // Reached the last message in the source file.
-          if (text !== null) {
-            // Array to hold indices of "From -" matches found within 'text'.
-            let lastPos = [];
-            // Regular expression to find "From - " at beginning of lines.
-            let regexpLast = /^(From - )/gm;
-            let resultLast = regexpLast.exec(text);
-            while (resultLast !== null) {
-              lastPos[lastPos.length] = resultLast.index;
-              resultLast = regexpLast.exec(text);
-            }
+/**
+ * Check if file is an mbox.
+ * (actually we can't really tell if it's an mbox or not just from the name.
+ * we just assume it is, if it's not .msf or .dat).
+ *
+ * @param {String} name     - Name of file to check.
+ * @returns {Boolean}       - true if file is an mbox
+ */
+function isMBoxName(name) {
+  let ext4 = name.substr(-4);
+  if (ext4 == ".msf" || ext4 == ".dat") {
+    return false;
+  }
+  // Assume all other files are mbox.
+  return true;
+}
 
-            // Create a maildir message file in 'dest/destFile/cur/'
-            // and open it for writing.
-            targetFile = OS.File.open(OS.Path.join(dest, destFile, "cur",
-              name.toString()), {write: true, create: true}, {});
-
-            // Extract the text in 'text' between 'lastPos[0]' ie the
-            // index of the first "From - " match and the end of 'text'.
-            targetFile.write(encoder.encode(text.substring(lastPos[0],
-              text.length)));
-            targetFile.close();
-
-            // Send a message indicating that a message was copied.
-            // This indicates progress for a pop account if the no. of msgs
-            // in the account is more than 0 and mailstore type is mbox.
-            self.postMessage(["copied", name, position]);
-          }
-
-          break;
-        }  else {
-          // We might have more messages in the source file.
-          prevPos = nextPos;
-          prevLen = nextLen;
-          text = textNew;
-        }
-
-        // Array to hold indices of "From -" matches found within 'text'.
-        let msgPos = [];
-        // Regular expression to find "From - " at beginning of lines.
-        let regexp = /^(From - )/gm;
-        let result = regexp.exec(text);
-        while (result !== null) {
-          msgPos[msgPos.length] = result.index;
-          result = regexp.exec(text);
-        }
+/**
+ * Check if directory is a maildir (by looking for a "cur" subdir).
+ *
+ * @param {String} dir    - Path of directory to check.
+ * @returns {Boolean}     - true if directory is a maildir.
+ */
+function isMaildir(dir) {
+  try {
+    let cur = OS.Path.join(dir, "cur");
+    let fi = OS.File.stat(cur);
+    return fi.isDir;
+  } catch (ex) {
+    if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+      // "cur" does not exist - not a maildir.
+      return false;
+    }
+    throw ex; // Other error.
+  }
+}
 
-        if (msgPos.length > 1) {
-          // More than one "From - " match is found.
-          noOfBytes = constNoOfBytes;
-          for (let i = 0; i < msgPos.length - 1; i++) {
-            // Create and open a new file in 'dest/destFile/cur'
-            // to hold the next mail.
-            targetFile = OS.File.open(OS.Path.join(dest, destFile, "cur",
-              name.toString()), {write: true, create: true}, {});
-            // Extract the text lying between consecutive indices, encode
-            // it and write it.
-            targetFile.write(encoder.encode(text.substring(msgPos[i],
-              msgPos[i + 1])));
-            targetFile.close();
-
-            // Send a message indicating that a mail was copied.
-            // This indicates progress for a pop account if the no. of msgs
-            // in the account is more than 0 and mailstore type is mbox.
-            self.postMessage(["copied", name, position + msgPos[i],
-              position + msgPos[i + 1]]);
+/**
+ * Count the number of messages in the "cur" dir of maildir.
+ *
+ * @param {String} maildir  - Path of maildir.
+ * @returns {Number}        - number of messages found.
+ */
+function countMaildirMsgs(maildir) {
+  let cur = OS.Path.join(maildir, "cur");
+  let iterator = new OS.File.DirectoryIterator(cur);
+  let count = 0;
+  try {
+    iterator.forEach(function(ent) { count++; });
+  } finally {
+    iterator.close();
+  }
+  return count;
+}
 
-            // Increment 'name' to get a new file name.
-            // Cannot use Date.now() because it is possible to get the
-            // same timestamp as before.
-            name++;
-
-            // Set index of the (i+1)th "From - " match found in 'text'.
-            lastMatchIndex = msgPos[i + 1];
-          }
-
-          // Now 'lastMatchIndex' holds the index of the last match found in
-          // 'text'. So we move the position in the file to 'position +
-          // lastMatchIndex' from the beginning of the file.
-          // This ensures that the next 'text' starts from "From - "
-          // and that there is at least 1 match every time.
-          sourceFileOpen.setPosition(position + lastMatchIndex,
-            OS.File.POS_START);
-        } else {
-          // If 1 match is found increase the no. of bytes to be extracted by
-          // 1000000 and move the position in the file to 'position', i.e. the
-          // position in the file before reading the bytes.
-          sourceFileOpen.setPosition(position, OS.File.POS_START);
-          noOfBytes = noOfBytes + 1000000;
+/**
+ * Recursively calculate the 'cost' of a hierarchy of maildir folders.
+ * This is the figure used for progress updates.
+ * For maildir, cost is 1 per message.
+ *
+ * @param {String} srcPath  - Path of root dir containing maildirs.
+ * @returns {Number}        - calculated conversion cost.
+ */
+function calcMaildirCost(srcPath) {
+  let cost = 0;
+  let iterator = new OS.File.DirectoryIterator(srcPath);
+  try {
+    iterator.forEach(function(ent) {
+      if (ent.isDir) {
+        if (isSBD(ent.name)) {
+          // Recurse into subfolder.
+          cost += calcMaildirCost(ent.path);
+        } else if (isMaildir(ent.path)) {
+          // Looks like a maildir. Cost is number of messages.
+          cost += countMaildirMsgs(ent.path);
         }
       }
+    });
+  } finally {
+    iterator.close();
+  }
+  return cost;
+}
 
-      // Send a message indicating that a message file was encountered.
-      // This indicates progress for an imap account if mailstore type is
-      // mbox.
-      // This indicates progress for a pop account if mailstore type is
-      // mbox and the no. of msgs in the account is 0.
-      self.postMessage(["file", sourceFile, textNew.length]);
-      return;
+/**
+ * Recursively calculate the 'cost' of a hierarchy of mbox folders.
+ * This is the figure used for progress updates.
+ * For mbox, cost is the total byte size of data. This avoids the need to
+ * parse the mbox files to count the number of messages.
+ * Note that this byte count cost is not 100% accurate because it includes
+ * the "From " lines which are not written into the maildir files. But it's
+ * definitely close enough to give good user feedback.
+ *
+ * @param {String} srcPath  - Path of root dir containing maildirs.
+ * @returns {Number}        - calculated conversion cost.
+ */
+function calcMBoxCost(srcPath) {
+  let cost = 0;
+  let iterator = new OS.File.DirectoryIterator(srcPath);
+  try {
+    iterator.forEach(function(ent) {
+      if (ent.isDir) {
+        if (isSBD(ent.name)) {
+          // Recurse into .sbd subfolder.
+          cost += calcMBoxCost(ent.path);
+        }
+      } else if (isMBoxName(ent.name)) {
+        let fi = OS.File.stat(ent.path);
+        cost += fi.size;
+      }
+    });
+  } finally {
+    iterator.close();
+  }
+  return cost;
+}
+
+/**
+ * Recursively convert a tree of mbox-based folders to maildirs.
+ *
+ * @param {String} srcPath              - Root path containing mboxes.
+ * @param {String} destPath             - Where to create destination root.
+ * @param {Function(Number)} progressFn - Function to be invoked regularly with
+ *                                        progress updates (called with number of
+ *                                        cost "units" since last update)
+ */
+function convertTreeMBoxToMaildir(srcPath, destPath, progressFn) {
+  OS.File.makeDir(destPath);
+
+  let iterator = new OS.File.DirectoryIterator(srcPath);
+  try {
+    iterator.forEach(function(ent) {
+      let dest = OS.Path.join(destPath, ent.name);
+      if (ent.isDir) {
+        if (isSBD(ent.name)) {
+          // Recurse into .sbd subfolder.
+          convertTreeMBoxToMaildir(ent.path, dest, progressFn);
+        }
+      } else if (isFileToCopy(ent.name)) {
+        OS.File.copy(ent.path, dest);
+      } else if (isMBoxName(ent.name)) {
+        // It's an mbox. Convert iterator.
+        mboxToMaildir(ent.path, dest, progressFn);
+      }
+    });
+  } finally {
+    iterator.close();
+  }
+}
+
+/**
+ * Recursively convert a tree of maildir-based folders to mbox.
+ *
+ * @param {String} srcPath              - Root path containing maildirs.
+ * @param {String} destPath             - Where to create destination root.
+ * @param {Function(Number)} progressFn - Function to be invoked regularly with
+ *                                        progress updates (called with number of
+ *                                        cost "units" since last update)
+ */
+function convertTreeMaildirToMBox(srcPath, destPath, progressFn) {
+  OS.File.makeDir(destPath);
+
+  let iterator = new OS.File.DirectoryIterator(srcPath);
+  try {
+    iterator.forEach(function(ent) {
+      let dest = OS.Path.join(destPath, ent.name);
+      if (ent.isDir) {
+        if (isSBD(ent.name)) {
+          // Recurse into .sbd subfolder.
+          convertTreeMaildirToMBox(ent.path, dest, progressFn);
+        } else if (isMaildir(ent.path)) {
+          // It's a maildir - convert iterator.
+          maildirToMBox(ent.path, dest, progressFn);
+        }
+      } else if (isFileToCopy(ent.name)) {
+        OS.File.copy(ent.path, dest);
+      }
+    });
+  } finally {
+    iterator.close();
+  }
+}
+
+self.addEventListener("message", function(e) {
+  try {
+    // Unpack the request params from the main thread.
+    let srcType = e.data.srcType;
+    let destType = e.data.destType;
+    let srcRoot = e.data.srcRoot;
+    let destRoot = e.data.destRoot;
+    // destRoot will be a temporary dir, so if it all goes pear-shaped
+    // we can just bail out without cleaning up.
+
+    // Configure the conversion.
+    let costFn = null;
+    let convertFn = null;
+    if (srcType == "maildir" && destType == "mbox") {
+      costFn = calcMaildirCost;
+      convertFn = convertTreeMaildirToMBox;
+    } else if (srcType == "mbox" && destType == "maildir") {
+      costFn = calcMBoxCost;
+      convertFn = convertTreeMBoxToMaildir;
+    } else {
+      throw new Error(`Unsupported conversion: ${srcType} => ${destType}`);
     }
 
-    // Should never get here, but the above rules are a little
-    // complex. So just in case.
-    throw new Error("Unhandled source: " + sourceFile);
+    // Go!
+    let totalCost = costFn(srcRoot);
+    let v = 0;
+    let progressFn = function(n) {
+      v += n;
+      self.postMessage({"msg": "progress", "val": v, "total": totalCost});
+    };
+    convertFn(srcRoot, destRoot, progressFn);
 
-  } catch (e) {
+    // We fake a final progress update, with exactly 100% completed.
+    // Our byte-counting on mbox->maildir conversion will fall slightly short:
+    // The total is estimated from the mbox filesize, but progress is tracked
+    // by counting bytes as they are written out - and the mbox "From " lines
+    // are _not_ written out to the maildir files.
+    // This is still accurate enough to provide progress to the user, but we
+    // don't want the GUI left showing "progress 97% - conversion complete!"
+    // or anything silly like that.
+    self.postMessage({"msg": "progress", "val": totalCost, "total": totalCost});
+
+    // Let the main thread know we succeeded.
+    self.postMessage({"msg": "success"});
+
+  } catch (err) {
     // We try-catch the error because otherwise the error from File.OS is
     // not properly propagated back to the worker error handling.
-    throw new Error(e);
+    throw new Error(err);
   }
 });
+
--- a/mailnews/base/util/mailstoreConverter.jsm
+++ b/mailnews/base/util/mailstoreConverter.jsm
@@ -1,531 +1,311 @@
 /* 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/. */
 
 var EXPORTED_SYMBOLS = ["convertMailStoreTo", "terminateWorkers"];
 
-ChromeUtils.import("resource:///modules/iteratorUtils.jsm");
-ChromeUtils.import("resource:///modules/MailUtils.jsm");
-ChromeUtils.import("resource:///modules/MailServices.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
 ChromeUtils.import("resource://gre/modules/osfile.jsm");
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 
-var log = Log.repository.getLogger("MailStoreConverter");
+let log = Log.repository.getLogger("MailStoreConverter");
 log.level = Log.Level.Debug;
 log.addAppender(new Log.DumpAppender(new Log.BasicFormatter()));
 
-// Array to hold workers.
-var gConverterWorkerArray = [];
+let gConverterWorker = null;
+
 
 /**
- * Creates "Converter" folder in tmp dir, moves the folder hierarchy of the
- * account root folder creating the same folder hierarchy in Converter
- * folder in tmp dir, copies the .msf and .dat files to proper places in
- * Converter folder, parses the mbox files and creates corresponding folders
- * and maildir files in proper places in "Converter" folder and returns a
- * promise.
- * @param aMailstoreContractId          - account mailstore contract id
- * @param {nsIMsgIncominserver} aServer - server for the account
- * @param aEventTarget                  - target on which the "progress"
- *                                        event will be dispatched
- * @return {Promise}                    - new root folder path of the converted
- *                                        server.
+ * Sets a server to use a different type of mailstore, converting
+ * all the existing data.
+ *
+ * @param {String} aMailstoreContractId - XPCOM id of new mailstore type.
+ * @param {nsIMsgServer} aServer        - server to migrate.
+ * @param aEventTarget                  - if set, element to send progress events.
+ *
+ * @returns {Promise} - Resolves with a string containing the new root
+ *                     directory for the migrated server.
+ *                     Rejects with a string error message.
  */
 function convertMailStoreTo(aMailstoreContractId, aServer, aEventTarget) {
-  // {nsIMsgfolder} account root folder.
-  var accountRootFolder = aServer.rootFolder.filePath;
-  // {nsIFile} tmp dir.
-  var tmpDir = FileUtils.getDir("TmpD", [], false);
-  // Array to hold path to the Converter folder in tmp dir.
-  var pathArray;
-  // No. of messages that have been copied in case of a pop account, movemail
-  // account, Local Folders account or any account with maildir mailstore type
-  // having at least 1 message.
-  // No. of files and folders that have been copied in case of a pop account,
-  // movemail account, Local Folders account or any account with maildir
-  // mailstore type having 0 messages.
-  // No. of files and folders that have been copied in case of an imap account
-  // or an nntp account.
-  var progressValue = 0;
-  // No. of files and folders in original account root folder for imap account
-  // if mailstore type is mbox, or an nntp account.
-  // No. of files and folders in original account root folder for a pop
-  // account, Local Folders account or movemail account if no. of msgs is 0
-  // and mailstore type is mbox.
-  // No. of files and folders in any non nntp account if no. of msgs is
-  // 0 and mailstore type is maildir.
-  // No. of messages in a pop account, Local Folders account or movemail
-  // account if no. of msgs is more than 0 and mailstore type is mbox.
-  // No. of messages in any non nntp account if no. of msgs is more than 0 and
-  // mailstore type is maildir.
-  var totalCount = 0;
-  // If there are zero msgs in the account "zeroMessages" is true else it is
-  // false.
-  var zeroMessages = false;
 
-  // No. of files and folders in original account root folder for imap account.
-  // We use a progress bar to show the status of the conversion process.
-  // So, we need a value as the maximum value of the progress bar to measure the
-  // progress.
-  // During the conversion there are three kinds of files or folders that can be
-  // encountered.
-  // 1. A directory - This simply requires a directory to be created in the right
-  //                  place. So this a single step.
-  // 2. A .msf or a .dat file - This simply requires the file to be copied to the
-  //                            right place. This too is a single step.
-  // 3. A message file - A message file contains several messages and each one
-  //                     needs to be copied to a separate file in the right
-  //                     place. So parsing a parsing a message file consists of
-  //                     several steps.
-  //
-  // So, it's the parsing of message files that is actually time consuming and
-  // dealing with a directory, .msf, .dat file takes very little time.
-  //
-  // So it makes more sense to measure progress as the no. of messages copied.
-  // But for an imap account, getTotalMessages(true) does not give the no. of
-  // messages actually present in the account root folder, but gives the no. of
-  // messages shown on Thunderbird which is less than the no. of messages
-  // actually present in the account root folder. So can't use that.
-  //
-  // But we still need a way to measure progress for an imap account.
-  // So we measure progress by the total no. of files and folders in the account
-  // root folder and we increment the value of the progress bar every time a
-  // .msf, .dat, or a message file or a directory is encountered during
-  // conversion.
-
-  /**
-   * Count no. of files and folders in the account root folder for imap
-   * accounts.
-   * @param {nsIMsgFolder} aFolder - account root folder.
-   */
-  var countImapFileFolders = function(aFolder) {
-    var count = 0;
-    var contents = aFolder.directoryEntries;
-    while (contents.hasMoreElements()) {
-      var content = contents.getNext()
-                            .QueryInterface(Ci.nsIFile);
-      if (content.isDirectory()) {
-        // Don't count Windows Search integration dir.
-        if (content.leafName.substr(-8) != ".mozmsgs") {
-          count = count + 1 + countImapFileFolders(content);
-        }
-      } else {
-        count++;
-      }
-    }
-    return count;
-  }
-
-  /**
-   * Count the no. of msgs in account root folder if the mailstore type is
-   * maildir.
-   * @param {nsIMsgFolder} aFolder - account root folder.
-   */
-  var countMaildirMsgs = function(aFolder) {
-    var count = 0;
-    var contents = aFolder.directoryEntries;
-    while (contents.hasMoreElements()) {
-      var content = contents.getNext().QueryInterface(Ci.nsIFile);
-      if (!content.isDirectory()) {
-        continue;
-      }
-      if (content.leafName.substr(-8) == ".mozmsgs") {
-        // Windows Search integration dir. Ignore.
-        continue;
-      }
-      if (content.leafName.substr(-4) == ".sbd") {
-        // A subfolder. Recurse into it.
-        count = count + countMaildirMsgs(content);
-      } else {
-        // We assume everything else is an actual maildir, and count the messages.
-        var cur = FileUtils.File(OS.Path.join(content.path,"cur"));
-        var curContents = cur.directoryEntries;
-        while (curContents.hasMoreElements()) {
-          curContents.getNext();
-          count++;
-        }
-      }
-    }
-    return count;
-  }
-
-  /**
-   * Count the no. of files and folders in account root folder if the mailstore
-   * type is maildir and the no. of msgs in the account is 0.
-   * @param {nsIMsgFolder} aFolder - account root folder.
-   */
-  var countMaildirZeroMsgs = function(aFolder) {
-    var count = 0;
-    var contents = aFolder.directoryEntries;
-    while (contents.hasMoreElements()) {
-      var content = contents.getNext().QueryInterface(Ci.nsIFile);
-      if (!content.isDirectory()) {
-        count++;
-      } else if (content.leafName.substr(-4) == ".sbd") {
-        // A subfolder. Recurse into it.
-        count = count + 1 + countMaildirMsgs(content);
-      } else if (content.leafName.substr(-8) != ".mozmsgs") {
-        // Assume any other dir is an actual maildir.
-        count++;
-      }
-    }
-    return count;
-  }
-
-  var isMaildir = (aMailstoreContractId == "@mozilla.org/msgstore/maildirstore;1");
-
-  var conversionOk; // Resolve callback function.
-  var conversionFailed; // Reject callback function.
-
-  /**
-   * Moves the folder hierarchy of the account root folder creating the same
-   * folder hierarchy in Converter folder in tmp dir, copies the .msf
-   * and .dat files to proper places in Converter folder, parses the mbox
-   * files and creates corresponding folders and maildir files in proper
-   * places in "Converter" folder and resolves the promise returned by
-   * convertmailStoreTo().
-   *
-   * @param {nsIFile} aFolder   - account root folder. Folder from where the
-   *                              files and directories are to be migrated.
-   * @param {nsIFile} aDestPath - "Converter" folder. Folder into which the
-   *                              files directories are to be migrated.
-   */
-  var subDir = function(aFolder, aDestPath) {
-    let contents = aFolder.directoryEntries;
-    // For each file in the source folder...
-    while (contents.hasMoreElements()) {
-      let content = contents.getNext()
-                            .QueryInterface(Ci.nsIFile);
-
-      // Data to be passed to the worker. Initially "dataArray" contains
-      // path of the directory in which the files and directories are to be
-      // migrated, path of the file or directory encountered, name of the file
-      // or directory encountered and the mailstore type, path of tmp dir,
-      // server type.
-      let dataArray = [
-        aDestPath.path,
-        content.path,
-        content.leafName,
-        aMailstoreContractId,
-        tmpDir.path,
-        aServer.type
-      ];
-
-      if (content.isDirectory()) {
-        if (content.leafName.substr(-4) != ".sbd" && content.leafName.substr(-8) != ".mozmsgs") {
-          // Assume it's a maildir, and grab the list of messages.
-          // Array to hold unsorted list of maildir msg filenames.
-          let dataArrayUnsorted = [];
-          // "cur" directory inside the maildir msg folder.
-          let cur = FileUtils.File(OS.Path.join(content.path,"cur"));
-          // Msg files inside "cur" directory.
-          let msgs = cur.directoryEntries;
-
-          while (msgs.hasMoreElements()) {
-            // Add filenames as integers into 'dataArrayUnsorted'.
-            // TODO: this'll break if maildir scheme changes! (eg .eml extension)
-            let msg = msgs.getNext()
-                          .QueryInterface(Ci.nsIFile);
-            dataArrayUnsorted.push(parseInt(msg.leafName));
-          }
-          dataArrayUnsorted.sort()
-          // Add the maildir msg filenames into 'dataArray' in a sorted order.
-          for (let elem of dataArrayUnsorted) {
-            dataArray.push(elem.toString());
-          }
-        }
-      }
-
-      // Set up the worker.
-      let converterWorker = new ChromeWorker(
-        "resource:///modules/converterWorker.js");
-      gConverterWorkerArray.push(converterWorker);
-      log.debug("Processing " + content.path + " => : " + aDestPath.path);
-
-      converterWorker.addEventListener("message", function(e) {
-        var responseWorker = e.data[0];
-        log.debug("Type of file or folder encountered: " + e.data);
+  let accountRootFolder = aServer.rootFolder.filePath;
 
-        // Dispatch the "progress" event on the event target and increment
-        // "progressValue" every time.
-        //
-        // mbox:
-        // - IMAP: a file or folder is copied.
-        //   This is because we cannot get the no. of messages actually present
-        //   in an imap account so we need some  other way to measure the
-        //   progress.
-        // - POP: a msg is copied if the no. of msgs in the account is more than
-        //   0. A file or folder is copied if the no. of msgs in the account is 0.
-        // - NNTP: a file or folder is copied.
-        // - MOVEMAIL: Same as POP.
-        // - NONE (LOCAL FOLDERS): Same as POP.
-        //
-        // maildir:
-        // - A msg is copied if the no. of msgs in the account is more than 0.
-        // - A file or folder is copied if the no. of msgs in the account is 0.
-        let popOrLocalOrMoveMailOrMaildir =
-           aServer.type == "pop3" || aServer.type == "none" ||
-           aServer.type == "movemail" || isMaildir;
-        if (((responseWorker == "copied" || (responseWorker != "copied" && zeroMessages))
-                                         && popOrLocalOrMoveMailOrMaildir)
-           ||
-             (responseWorker != "copied" && !popOrLocalOrMoveMailOrMaildir)
-           ||
-             (responseWorker != "copied" && aServer.type == "nntp")
-           ) {
-          progressValue++;
-          log.debug("Progress: " + progressValue);
-
-          let event = new Event("progress");
-          event.detail = parseInt((progressValue/totalCount) * 100);
-          aEventTarget.dispatchEvent(event);
-          if (progressValue == totalCount) {
-            log.info("Migration completed. Migrated " + totalCount + " items");
-
-            // Migration is complete, get path of parent of account root
-            // folder into "parentPath" check if Converter folder already
-            // exists in "parentPath". If yes, remove it.
-            let lastSlash = accountRootFolder.path.lastIndexOf("/");
-            let parentPath = accountRootFolder.parent.path;
-            log.info("Path to parent folder of account root" +
-                     " folder: " + parentPath);
-
-            let parent = new FileUtils.File(parentPath);
-            log.info("Path to parent folder of account root folder: " +
-              parent.path);
-
-            var converterFolder = new FileUtils.File(OS.Path.join(parent.path,
-              dir.leafName));
-            if (converterFolder.exists()) {
-              log.info("Converter folder exists in " + parentPath +
-                       ". Removing already existing folder");
-              converterFolder.remove(true);
-            }
-
-            // Move Converter folder into the parent of account root folder.
-            try {
-              dir.moveTo(parent, dir.leafName);
-              // {nsIFile} new account root folder.
-              var newRootFolder = new FileUtils.File(OS.Path.join(parent.path,
-                dir.leafName));
-              log.info("Path to new account root folder: " +
-                       newRootFolder.path);
-            } catch (e) {
-              // Cleanup.
-              log.error(e);
-              var newRootFolder = new FileUtils.File(OS.Path.join(parent.path,
-                dir.leafName));
-              log.error("Trying to remove converter folder: " +
-                newRootFolder.path);
-              newRootFolder.remove(true);
-              conversionFailed(e);
-            }
-
-            // If the account is imap then copy the msf file for the original
-            // root folder and rename the copy with the name of the new root
-            // folder.
-            if (aServer.type != "pop3" && aServer.type != "none" &&
-              aServer.type != "movemail") {
-              let converterFolderMsf = new FileUtils.File(OS.Path.join(
-                parent.path,dir.leafName + ".msf"));
-              if (converterFolderMsf.exists()) {
-                converterFolderMsf.remove(true);
-              }
-
-              let oldRootFolderMsf = new FileUtils.File(OS.Path.join(
-                parent.path,accountRootFolder.leafName + ".msf"));
-              if (oldRootFolderMsf.exists()) {
-                oldRootFolderMsf.copyTo(parent, converterFolderMsf.leafName);
-              }
-            }
-
-            if (aServer.type == "nntp") {
-              let converterFolderNewsrc = new FileUtils.File(OS.Path.join(
-                parent.path,"newsrc-" + dir.leafName));
-              if (converterFolderNewsrc.exists()) {
-                converterFolderNewsrc.remove(true);
-              }
-              let oldNewsrc = new FileUtils.File(OS.Path.join(parent.path,
-                "newsrc-" + accountRootFolder.leafName));
-              if (oldNewsrc.exists()) {
-                oldNewsrc.copyTo(parent, converterFolderNewsrc.leafName);
-              }
-            }
-
-            aServer.rootFolder.filePath = newRootFolder;
-            aServer.localPath = newRootFolder;
-            log.info("Path to account root folder: " +
-                     aServer.rootFolder.filePath.path);
-
-            // Set various preferences.
-            let p1 = "mail.server." + aServer.key + ".directory";
-            let p2 = "mail.server." + aServer.key + ".directory-rel";
-            let p3 = "mail.server." + aServer.key + ".newsrc.file";
-            let p4 = "mail.server." + aServer.key + ".newsrc.file-rel";
-            let p5 = "mail.server." + aServer.key + ".storeContractID";
-
-            Services.prefs.setCharPref(p1, newRootFolder.path);
-            log.info(p1 + ": " + newRootFolder.path)
-
-            // The directory-rel pref is of the form "[ProfD]Mail/pop.gmail.com
-            // " (pop accounts) or "[ProfD]ImapMail/imap.gmail.com" (imap
-            // accounts) ie the last slash "/" is followed by the root folder
-            // name. So, replace the old root folder name that follows the last
-            // slash with the new root folder name to set the correct value of
-            // directory-rel pref.
-            let directoryRel = Services.prefs.getCharPref(p2);
-            lastSlash = directoryRel.lastIndexOf("/");
-            directoryRel = directoryRel.slice(0, lastSlash) + "/" +
-                                              newRootFolder.leafName;
-            Services.prefs.setCharPref(p2, directoryRel);
-            log.info(p2 + ": " + directoryRel);
-
-            if (aServer.type == "nntp") {
-              let newNewsrc = FileUtils.File(OS.Path.join(parent.path,
-                "newsrc-" + newRootFolder.leafName));
-              Services.prefs.setCharPref(p3, newNewsrc.path);
-
-              // The newsrc.file-rel pref is of the form "[ProfD]News/newsrc-
-              // news.mozilla.org" ie the last slash "/" is followed by the
-              // newsrc file name. So, replace the old newsrc file name that
-              // follows the last slash with the new newsrc file name to set
-              // the correct value of newsrc.file-rel pref.
-              let newsrcRel = Services.prefs.getCharPref(p4);
-              lastSlash = newsrcRel.lastIndexOf("/");
-              newsrcRel = newsrcRel.slice(0, lastSlash) + "/" +
-                                          newNewsrc.leafName;
-              Services.prefs.setCharPref(p4, newsrcRel);
-              log.info(p4 + ": " + newsrcRel);
-            }
-
-            Services.prefs.setCharPref(p5, isMaildir ?
-              "@mozilla.org/msgstore/berkeleystore;1" :
-              "@mozilla.org/msgstore/maildirstore;1");
-
-            Services.prefs.savePrefFile(null);
-            log.info("Conversion done!");
-
-            // Resolve the promise with the path of the new account root
-            // folder.
-            conversionOk(newRootFolder.path);
-          }
-        }
-      });
-
-      converterWorker.addEventListener("error", function(e) {
-        let reasonString =
-          "Error at " + e.filename + ":" + e.lineno + " - " +  e.message;
-        log.error(reasonString);
-        terminateWorkers();
-        // Cleanup.
-        log.error("Trying to remove converter folder: " +
-          aDestPath.path);
-        aDestPath.remove(true);
-        conversionFailed(e.message);
-      });
-
-      // Kick off the worker.
-      converterWorker.postMessage(dataArray);
-
-      if (content.isDirectory()) {
-        if (content.leafName.substr(-4) == ".sbd") {
-          let dirNew = new FileUtils.File(OS.Path.join(aDestPath.path,
-            content.leafName));
-          subDir(content, dirNew);
-        }
-      }
-    }
+  let srcType = null;
+  let destType = null;
+  if (aMailstoreContractId == "@mozilla.org/msgstore/maildirstore;1") {
+    srcType = "maildir";
+    destType = "mbox";
+  } else {
+    srcType = "mbox";
+    destType = "maildir";
   }
 
-  /**
-   * Checks if Converter folder exists in tmp dir, removes it and creates a new
-   * "Converter" folder.
-   * @param {nsIFile} aFolder - account root folder.
-   */
-  var createTmpConverterFolder = function(aFolder) {
-    let tmpFolder;
-    switch (aMailstoreContractId) {
-      case "@mozilla.org/msgstore/maildirstore;1": {
-        if (aFolder.leafName.substr(-8) == "-maildir") {
-          tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
-            aFolder.leafName.substr(0, aFolder.leafName.length - 8) + "-mbox"));
-        } else {
-          tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
-            aFolder.leafName + "-mbox"));
-        }
-
-        if (tmpFolder.exists()) {
-          log.info("Temporary Converter folder " + tmpFolder.path +
-                   "exists in tmp dir. Removing it");
-          tmpFolder.remove(true);
-        }
-        return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
-      }
-
-      case "@mozilla.org/msgstore/berkeleystore;1": {
-        if (aFolder.leafName.substr(-5) == "-mbox") {
-          tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
-            aFolder.leafName.substr(0, aFolder.leafName.length - 5) +
-              "-maildir"));
-        } else {
-          tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
-            aFolder.leafName + "-maildir"));
-        }
-
-        if (tmpFolder.exists()) {
-          log.info("Temporary Converter folder " + tmpFolder.path +
-                   "exists in tmp dir. Removing it");
-          tmpFolder.remove(true);
-        }
-        return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
-      }
-
-      default: {
-        throw new Error("Unexpected mailstoreContractId: " +
-                        aMailstoreContractId);
-      }
-    }
-  }
-
-  if (isMaildir && aServer.type != "nntp") {
-
-    // TODO: why can't maildir count use aServer.rootFolder.getTotalMessages(true)?
-    totalCount = countMaildirMsgs(accountRootFolder);
-    if (totalCount == 0) {
-      totalCount = countMaildirZeroMsgs(accountRootFolder);
-      zeroMessages = true;
-    }
-  } else if (aServer.type == "pop3" ||
-             aServer.type == "none" || // none: Local Folders.
-             aServer.type == "movemail") {
-    totalCount = aServer.rootFolder.getTotalMessages(true);
-    if (totalCount == 0) {
-      totalCount = countImapFileFolders(accountRootFolder);
-      zeroMessages = true;
-    }
-  } else if (aServer.type == "imap" || aServer.type == "nntp") {
-    totalCount = countImapFileFolders(accountRootFolder);
-  }
-  log.debug("totalCount = " + totalCount + " (zeroMessages = " + zeroMessages + ")");
-
   // Go offline before conversion, so there aren't messages coming in during
   // the process.
   Services.io.offline = true;
-  let dir = createTmpConverterFolder(accountRootFolder);
+  let destDir = createTmpConverterFolder(accountRootFolder, aMailstoreContractId);
+
+  // Return a promise that will complete once the worker is done.
   return new Promise(function(resolve, reject) {
-    conversionOk = resolve;
-    conversionFailed = reject;
-    subDir(accountRootFolder, dir);
+
+    let worker = new ChromeWorker("resource:///modules/converterWorker.js");
+    gConverterWorker = worker;
+
+    // Helper to log error, clean up and return failure.
+    let bailout = function(e) {
+      let reasonString =
+        "ERROR " + e.filename + ":" + e.lineno + " - " + e.message;
+      log.error(reasonString);
+      // Cleanup.
+      log.error("Trying to remove converter folder: " + destDir.path);
+      destDir.remove(true);
+      reject(e.message);
+    };
+
+    // Handle exceptions thrown by the worker thread.
+    worker.addEventListener("error", bailout);
+
+    // Handle updates from the worker thread.
+    worker.addEventListener("message", function(e) {
+      let response = e.data;
+      // log.debug("WORKER SAYS: " + JSON.stringify(response) + "\n");
+      if (response.msg == "progress") {
+        let val = response.val;
+        let total = response.total;
+
+        // Send the percentage completion to the GUI.
+        // XXX TODO: should probably check elapsed time, and throttle
+        // the events to avoid spending all our time drawing!
+        let ev = new Event("progress");
+        ev.detail = parseInt((val / total) * 100);
+        if (aEventTarget) {
+          aEventTarget.dispatchEvent(ev);
+        }
+      }
+      if (response.msg == "success") {
+        // If we receive this, the worker has completed, without errors.
+        let storeTypeIDs = {
+          "mbox": "@mozilla.org/msgstore/berkeleystore;1",
+          "maildir": "@mozilla.org/msgstore/maildirstore;1",
+        };
+        let newStoreTypeID = storeTypeIDs[destType];
+
+        try {
+          let finalRoot = installNewRoot(aServer, destDir, newStoreTypeID);
+          log.info("Conversion complete. Converted dir installed as: " + finalRoot);
+          resolve(finalRoot);
+        } catch (e) {
+          bailout(e);
+        }
+      }
+    });
+
+    // Kick off the worker.
+    worker.postMessage({
+      "srcType": srcType,
+      "destType": destType,
+      "srcRoot": accountRootFolder.path,
+      "destRoot": destDir.path,
+    });
   });
 }
 
 /**
- * Terminate all workers.
+ * Checks if Converter folder exists in tmp dir, removes it and creates a new
+ * "Converter" folder.
+ * @param {nsIFile} aFolder             - account root folder.
+ * @param {String} aMailstoreContractId - XPCOM id of dest mailstore type
+ *
+ * @returns {nsIFile} - the new tmp directory to use as converter dest.
+ */
+function createTmpConverterFolder(aFolder, aMailstoreContractId) {
+  let tmpDir = FileUtils.getDir("TmpD", [], false);
+  let tmpFolder;
+  switch (aMailstoreContractId) {
+    case "@mozilla.org/msgstore/maildirstore;1": {
+      if (aFolder.leafName.substr(-8) == "-maildir") {
+        tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
+          aFolder.leafName.substr(0, aFolder.leafName.length - 8) + "-mbox"));
+      } else {
+        tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
+          aFolder.leafName + "-mbox"));
+      }
+
+      if (tmpFolder.exists()) {
+        log.info("Temporary Converter folder " + tmpFolder.path +
+                  " exists in tmp dir. Removing it");
+        tmpFolder.remove(true);
+      }
+      return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
+    }
+
+    case "@mozilla.org/msgstore/berkeleystore;1": {
+      if (aFolder.leafName.substr(-5) == "-mbox") {
+        tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
+          aFolder.leafName.substr(0, aFolder.leafName.length - 5) +
+            "-maildir"));
+      } else {
+        tmpFolder = new FileUtils.File(OS.Path.join(tmpDir.path,
+          aFolder.leafName + "-maildir"));
+      }
+
+      if (tmpFolder.exists()) {
+        log.info("Temporary Converter folder " + tmpFolder.path +
+                  "exists in tmp dir. Removing it");
+        tmpFolder.remove(true);
+      }
+      return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
+    }
+
+    default: {
+      throw new Error("Unexpected mailstoreContractId: " +
+                      aMailstoreContractId);
+    }
+  }
+}
+
+
+/**
+ * Switch server over to use the newly-converted directory tree.
+ * Moves the converted directory into an appropriate place for the server.
+ *
+ * @param {nsIMsgServer} server   - server to migrate.
+ * @param {String} dir            - dir of converted mailstore to install
+ *                                  (will be moved by this function).
+ * @param {String} newStoreTypeID - XPCOM id of new mailstore type.
+ * @returns {String} new location of dir.
+ */
+function installNewRoot(server, dir, newStoreTypeID) {
+  let accountRootFolder = server.rootFolder.filePath;
+
+  // Migration is complete, get path of parent of account root
+  // folder into "parentPath" check if Converter folder already
+  // exists in "parentPath". If yes, remove it.
+  let lastSlash = accountRootFolder.path.lastIndexOf("/");
+  let parentPath = accountRootFolder.parent.path;
+  log.info("Path to parent folder of account root" +
+            " folder: " + parentPath);
+
+  let parent = new FileUtils.File(parentPath);
+  log.info("Path to parent folder of account root folder: " +
+    parent.path);
+
+  let converterFolder = new FileUtils.File(OS.Path.join(parent.path,
+    dir.leafName));
+  if (converterFolder.exists()) {
+    log.info("Converter folder exists in " + parentPath +
+              ". Removing already existing folder");
+    converterFolder.remove(true);
+  }
+
+  // Move Converter folder into the parent of account root folder.
+  try {
+    dir.moveTo(parent, dir.leafName);
+    // {nsIFile} new account root folder.
+    log.info("Path to new account root folder: " +
+              converterFolder.path);
+  } catch (e) {
+    // Cleanup.
+    log.error(e);
+    log.error("Trying to remove converter folder: " +
+      converterFolder.path);
+    converterFolder.remove(true);
+    throw e;
+  }
+
+  // If the account is imap then copy the msf file for the original
+  // root folder and rename the copy with the name of the new root
+  // folder.
+  if (server.type != "pop3" && server.type != "none" &&
+    server.type != "movemail") {
+    let converterFolderMsf = new FileUtils.File(OS.Path.join(
+      parent.path, dir.leafName + ".msf"));
+    if (converterFolderMsf.exists()) {
+      converterFolderMsf.remove(true);
+    }
+
+    let oldRootFolderMsf = new FileUtils.File(OS.Path.join(
+      parent.path, accountRootFolder.leafName + ".msf"));
+    if (oldRootFolderMsf.exists()) {
+      oldRootFolderMsf.copyTo(parent, converterFolderMsf.leafName);
+    }
+  }
+
+  if (server.type == "nntp") {
+    let converterFolderNewsrc = new FileUtils.File(OS.Path.join(
+      parent.path, "newsrc-" + dir.leafName));
+    if (converterFolderNewsrc.exists()) {
+      converterFolderNewsrc.remove(true);
+    }
+    let oldNewsrc = new FileUtils.File(OS.Path.join(parent.path,
+      "newsrc-" + accountRootFolder.leafName));
+    if (oldNewsrc.exists()) {
+      oldNewsrc.copyTo(parent, converterFolderNewsrc.leafName);
+    }
+  }
+
+  server.rootFolder.filePath = converterFolder;
+  server.localPath = converterFolder;
+  log.info("Path to account root folder: " +
+            server.rootFolder.filePath.path);
+
+  // Set various preferences.
+  let p1 = "mail.server." + server.key + ".directory";
+  let p2 = "mail.server." + server.key + ".directory-rel";
+  let p3 = "mail.server." + server.key + ".newsrc.file";
+  let p4 = "mail.server." + server.key + ".newsrc.file-rel";
+  let p5 = "mail.server." + server.key + ".storeContractID";
+
+  Services.prefs.setCharPref(p1, converterFolder.path);
+  log.info(p1 + ": " + converterFolder.path);
+
+  // The directory-rel pref is of the form "[ProfD]Mail/pop.gmail.com
+  // " (pop accounts) or "[ProfD]ImapMail/imap.gmail.com" (imap
+  // accounts) ie the last slash "/" is followed by the root folder
+  // name. So, replace the old root folder name that follows the last
+  // slash with the new root folder name to set the correct value of
+  // directory-rel pref.
+  let directoryRel = Services.prefs.getCharPref(p2);
+  lastSlash = directoryRel.lastIndexOf("/");
+  directoryRel = directoryRel.slice(0, lastSlash) + "/" +
+                                    converterFolder.leafName;
+  Services.prefs.setCharPref(p2, directoryRel);
+  log.info(p2 + ": " + directoryRel);
+
+  if (server.type == "nntp") {
+    let newNewsrc = FileUtils.File(OS.Path.join(parent.path,
+      "newsrc-" + converterFolder.leafName));
+    Services.prefs.setCharPref(p3, newNewsrc.path);
+
+    // The newsrc.file-rel pref is of the form "[ProfD]News/newsrc-
+    // news.mozilla.org" ie the last slash "/" is followed by the
+    // newsrc file name. So, replace the old newsrc file name that
+    // follows the last slash with the new newsrc file name to set
+    // the correct value of newsrc.file-rel pref.
+    let newsrcRel = Services.prefs.getCharPref(p4);
+    lastSlash = newsrcRel.lastIndexOf("/");
+    newsrcRel = newsrcRel.slice(0, lastSlash) + "/" +
+                                newNewsrc.leafName;
+    Services.prefs.setCharPref(p4, newsrcRel);
+    log.info(p4 + ": " + newsrcRel);
+  }
+
+  Services.prefs.setCharPref(p5, newStoreTypeID);
+
+  Services.prefs.savePrefFile(null);
+
+  return converterFolder.path;
+}
+
+/**
+ * Terminate any workers involved in the conversion process.
  */
 function terminateWorkers() {
-  for (let worker of gConverterWorkerArray) {
-    worker.terminate();
+  // We're only using a single worker right now.
+  if (gConverterWorker !== null) {
+    gConverterWorker.terminate();
+    gConverterWorker = null;
   }
 }
new file mode 100644
--- /dev/null
+++ b/mailnews/test/data/mbox_mboxrd
@@ -0,0 +1,15 @@
+From MAILER-DAEMON Fri Jul  8 12:08:34 2011
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 1
+
+This is the body.
+>From (should be escaped).
+There are 3 lines.
+
+From MAILER-DAEMON Fri Jul  8 12:08:34 2011
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 2
+
+This is the second body.
new file mode 100644
--- /dev/null
+++ b/mailnews/test/data/mbox_modern
@@ -0,0 +1,20 @@
+From - Fri Aug 24 11:55:47 2018
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 1
+Date: Fri, 24 Aug 2018 11:55:47 +0000
+
+Later versions of Thunderbird quote things by prefixing
+a space,like this:
+ From 
+ From - Fri Aug 24 11:55:47 2018
+This could cause problems, if a reader decides to split the message
+here.
+
+From - Thu Aug 23 09:10:23 2018
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 2
+Date: Thu, 23 Aug 2018 09:10:23 +0000
+
+This is the second body.
new file mode 100644
--- /dev/null
+++ b/mailnews/test/data/mbox_unquoted
@@ -0,0 +1,18 @@
+From 
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 1
+Date: Wed, 22 Aug 2005 17:20:08 +0000
+
+Earlier versions of Thunderbird don't seem to quote things like this:
+From 
+This could cause problems, if a reader decides to split the message
+here.
+
+From 
+From: Author <author@example.com>
+To: Recipient <recipient@example.com>
+Subject: Sample message 2
+Date: Thu, 23 Aug 2005 11:17:43 +0000
+
+This is the second body.
--- a/mailnews/test/data/readme.txt
+++ b/mailnews/test/data/readme.txt
@@ -54,8 +54,50 @@ This is a file which is known as the MIM
 nested multipart/* and message/* segments; as its explanation describes:
 
   This is a demonstration of multi-part mail with encapsulated messages.  This
   is a very complex message whose purpose it is to exercise software using the
   new multi-part message standard.
 
 The original source of this file is unknown, but a copy can be found at
 <http://sourceforge.net/projects/kmmail/files/MIME%20Torture%20Tests/>.
+
+
+
+
+mbox_modern
+-----------
+
+A simple mbox in the form that Thunderbird currently seems to save (as
+of Nov 2018).
+Separator lines are of the form:
+
+From - Fri Aug 24 11:55:47 2018
+
+Lines beginning with "From " within the message body are escaped
+by prefixing a space.
+
+
+mbox_unquoted
+-------------
+
+Old-style mbox. This seems to be what Thunderbird was storing back around
+2005, and so there are likely to be a bunch of these out in the wild.
+
+Separator lines are lines containing only: "From "
+Lines within the message body beginning with "From " are not escaped in
+any way.
+
+
+mbox_mboxrd
+-----------
+
+Example of the mboxrd variant, taken from the wikipedia mbox page.
+Separator lines are of the form:
+
+From MAILER-DAEMON Fri Jul  8 12:08:34 2011
+
+(note that MAILER-DAEMON is a special case - this is where an
+email address usually goes, but MAILER-DAEMON can be used instead).
+
+"From " lines within the message body are escaped by prefixing with
+a '>' char.
+