mail/base/modules/MessageArchiver.jsm
author Mozilla Releng Treescript <release+treescript@mozilla.org>
Thu, 01 Oct 2020 13:10:22 +0000
changeset 36219 0f2fa82267ad428eff0a4d6029db701185dfd57c
parent 35840 0111a56e22d53bdc386a787dc55ad5b8abe555b1
permissions -rw-r--r--
Automatic version bump CLOSED TREE NO BUG a=release DONTBUILD

/* 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) {
      // Continue processing.
      this.processNextBatch();
    }

    // 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) {
        // Continues with OnStopRunningUrl.
        return;
      }
    }

    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) {
          // Continues with OnStopRunningUrl.
          return;
        }
      }
    }
    if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders) {
      archiveFolderURI += "/" + batch.monthFolderName;
      dstFolder = MailUtils.getOrCreateFolder(archiveFolderURI);
      if (!dstFolder.parent) {
        dstFolder.createStorageIfMissing(this);
        if (isAsync) {
          // Continues with OnStopRunningUrl.
          return;
        }
      }
    }

    // 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) {
            // Continues with folderAdded.
            return;
          }
        }
        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",
  ]),
};