summaryrefslogtreecommitdiffstats
path: root/comm/mail/modules/MessageArchiver.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/modules/MessageArchiver.jsm')
-rw-r--r--comm/mail/modules/MessageArchiver.jsm392
1 files changed, 392 insertions, 0 deletions
diff --git a/comm/mail/modules/MessageArchiver.jsm b/comm/mail/modules/MessageArchiver.jsm
new file mode 100644
index 0000000000..bf13a17295
--- /dev/null
+++ b/comm/mail/modules/MessageArchiver.jsm
@@ -0,0 +1,392 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["MessageArchiver"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "MailUtils",
+ "resource:///modules/MailUtils.jsm"
+);
+
+function MessageArchiver() {
+ this._batches = {};
+ this._currentKey = null;
+ this._dstFolderParent = null;
+ this._dstFolderName = null;
+
+ this.msgWindow = null;
+ this.oncomplete = null;
+}
+
+/**
+ * The maximum number of messages to try to examine directly to determine if
+ * they can be archived; if we exceed this count, we'll try to approximate
+ * the answer by looking at the server's identities. This is only here to
+ * let tests tweak the value.
+ */
+MessageArchiver.MAX_COUNT_FOR_CAN_ARCHIVE_CHECK = 100;
+MessageArchiver.canArchive = function (messages, isSingleFolder) {
+ if (messages.length == 0) {
+ return false;
+ }
+
+ // If we're looking at a single folder (i.e. not a cross-folder search), we
+ // can just check to see if all the identities for this folder/server have
+ // archives enabled (or disabled). This is way faster than checking every
+ // message. Note: this may be slightly inaccurate if the identity for a
+ // header is actually on another server.
+ if (
+ messages.length > MessageArchiver.MAX_COUNT_FOR_CAN_ARCHIVE_CHECK &&
+ isSingleFolder
+ ) {
+ let folder = messages[0].folder;
+ let folderIdentity = folder.customIdentity;
+ if (folderIdentity) {
+ return folderIdentity.archiveEnabled;
+ }
+
+ if (folder.server) {
+ let serverIdentities = MailServices.accounts.getIdentitiesForServer(
+ folder.server
+ );
+
+ // Do all identities have the same archiveEnabled setting?
+ if (serverIdentities.every(id => id.archiveEnabled)) {
+ return true;
+ }
+ if (serverIdentities.every(id => !id.archiveEnabled)) {
+ return false;
+ }
+ // If we get here it's a mixture, so have to examine all the messages.
+ }
+ }
+
+ // Either we've selected a small number of messages or we just can't
+ // fast-path the result; examine all the messages.
+ return messages.every(function (msg) {
+ let [identity] = lazy.MailUtils.getIdentityForHeader(msg);
+ return Boolean(identity && identity.archiveEnabled);
+ });
+};
+
+// Bad things happen if you have multiple archivers running on the same
+// messages (See Bug 1705824). We could probably make this more fine
+// grained, and maintain a list of messages/folders already queued up...
+// but that'd get complex quick, so let's keep things simple for now and
+// only allow one active archiver.
+let gIsArchiving = false;
+
+MessageArchiver.prototype = {
+ archiveMessages(aMsgHdrs) {
+ if (!aMsgHdrs.length) {
+ return;
+ }
+ if (gIsArchiving) {
+ throw new Error("Can only have one MessageArchiver running at once");
+ }
+ gIsArchiving = true;
+
+ 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] = lazy.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;
+ }
+ // All done!
+ this._batches = null;
+ MailServices.mfn.removeListener(this);
+
+ if (typeof this.oncomplete == "function") {
+ this.oncomplete();
+ }
+ gIsArchiving = false;
+ },
+
+ filterBatch() {
+ let batch = this._currentBatch;
+ // Apply filters to this batch.
+ MailServices.filters.applyFilters(
+ Ci.nsMsgFilterType.Archive,
+ batch.messages,
+ batch.srcFolder,
+ this.msgWindow,
+ this
+ );
+ // continues with onStopOperation
+ },
+
+ onStopOperation(aResult) {
+ if (!Components.isSuccessCode(aResult)) {
+ console.error("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 = lazy.MailUtils.getOrCreateFolder(archiveFolderURI);
+ let dstFolder = archiveFolder;
+
+ let moveArray = [];
+ // 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.push(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 = lazy.MailUtils.getOrCreateFolder(archiveFolderURI);
+ if (!dstFolder.parent) {
+ dstFolder.createStorageIfMissing(this);
+ if (isAsync) {
+ // Continues with OnStopRunningUrl.
+ return;
+ }
+ }
+ }
+ if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders) {
+ archiveFolderURI += "/" + batch.monthFolderName;
+ dstFolder = lazy.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 = lazy.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) {
+ let isNews = srcFolder.flags & Ci.nsMsgFolderFlags.Newsgroup;
+ // 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 && !isNews,
+ 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 {
+ console.error("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
+ console.error("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",
+ ]),
+};