summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/content/junkCommands.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/content/junkCommands.js')
-rw-r--r--comm/mailnews/base/content/junkCommands.js449
1 files changed, 449 insertions, 0 deletions
diff --git a/comm/mailnews/base/content/junkCommands.js b/comm/mailnews/base/content/junkCommands.js
new file mode 100644
index 0000000000..1554d54256
--- /dev/null
+++ b/comm/mailnews/base/content/junkCommands.js
@@ -0,0 +1,449 @@
+/* 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/. */
+
+/**
+ * Functions use for junk processing commands
+ */
+
+/*
+ * TODO: These functions make the false assumption that a view only contains
+ * a single folder. This is not true for XF saved searches.
+ *
+ * globals prerequisites used:
+ *
+ * top.window.MsgStatusFeedback
+ */
+
+/* globals gDBView, gViewWrapper */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "MailUtils",
+ "resource:///modules/MailUtils.jsm"
+);
+
+/**
+ * Determines the actions that should be carried out on the messages
+ * that are being marked as junk
+ *
+ * @param {nsIMsgFolder} aFolder - The folder with messages being marked as junk.
+ * @returns {object} result an object with two properties.
+ * @returns {boolean} result.markRead - Whether the messages should be marked
+ * as read.
+ * @returns {?nsIMsgFolder} result.junkTargetFolder - Where the messages should
+ * be moved, or null if they should not be moved.
+ */
+function determineActionsForJunkMsgs(aFolder) {
+ var actions = { markRead: false, junkTargetFolder: null };
+ var spamSettings = aFolder.server.spamSettings;
+
+ // note we will do moves/marking as read even if the spam
+ // feature is disabled, since the user has asked to use it
+ // despite the disabling
+
+ actions.markRead = spamSettings.markAsReadOnSpam;
+ actions.junkTargetFolder = null;
+
+ // move only when the corresponding setting is activated
+ // and the currently viewed folder is not the junk folder.
+ if (spamSettings.moveOnSpam && !aFolder.getFlag(Ci.nsMsgFolderFlags.Junk)) {
+ var spamFolderURI = spamSettings.spamFolderURI;
+ if (!spamFolderURI) {
+ // XXX TODO
+ // we should use nsIPromptService to inform the user of the problem,
+ // e.g. when the junk folder was accidentally deleted.
+ dump("determineActionsForJunkMsgs: no spam folder found, not moving.");
+ } else {
+ actions.junkTargetFolder = MailUtils.getOrCreateFolder(spamFolderURI);
+ }
+ }
+
+ return actions;
+}
+
+/**
+ * Performs required operations on a list of newly-classified junk messages.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder with messages being marked as
+ * junk.
+ * @param {nsIMsgDBHdr[]} aJunkMsgHdrs - New junk messages.
+ * @param {nsIMsgDBHdr[]} aGoodMsgHdrs - New good messages.
+ */
+async function performActionsOnJunkMsgs(aFolder, aJunkMsgHdrs, aGoodMsgHdrs) {
+ return new Promise((resolve, reject) => {
+ if (aFolder instanceof Ci.nsIMsgImapMailFolder) {
+ // need to update IMAP custom flags
+ if (aJunkMsgHdrs.length) {
+ let junkMsgKeys = aJunkMsgHdrs.map(hdr => hdr.messageKey);
+ aFolder.storeCustomKeywords(null, "Junk", "NonJunk", junkMsgKeys);
+ }
+
+ if (aGoodMsgHdrs.length) {
+ let goodMsgKeys = aGoodMsgHdrs.map(hdr => hdr.messageKey);
+ aFolder.storeCustomKeywords(null, "NonJunk", "Junk", goodMsgKeys);
+ }
+ }
+ if (!aJunkMsgHdrs.length) {
+ resolve();
+ return;
+ }
+
+ let actionParams = determineActionsForJunkMsgs(aFolder);
+ if (actionParams.markRead) {
+ aFolder.markMessagesRead(aJunkMsgHdrs, true);
+ }
+
+ if (!actionParams.junkTargetFolder) {
+ resolve();
+ return;
+ }
+
+ // @implements {nsIMsgCopyServiceListener}
+ let listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]),
+ OnStartCopy() {},
+ OnProgress(progress, progressMax) {},
+ SetMessageKey(key) {},
+ GetMessageId() {},
+ OnStopCopy(status) {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ return;
+ }
+ let uri = actionParams.junkTargetFolder.URI;
+ reject(new Error(`Moving junk to ${uri} failed.`));
+ },
+ };
+ MailServices.copy.copyMessages(
+ aFolder,
+ aJunkMsgHdrs,
+ actionParams.junkTargetFolder,
+ true /* isMove */,
+ listener,
+ top.msgWindow,
+ true /* allow undo */
+ );
+ });
+}
+
+/**
+ * Helper object storing the list of pending messages to process,
+ * and implementing junk processing callback.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder with messages to be analyzed for junk.
+ * @param {integer} aTotalMessages - Number of messages to process, used for
+ * progress report only.
+ */
+
+function MessageClassifier(aFolder, aTotalMessages) {
+ this.mFolder = aFolder;
+ this.mJunkMsgHdrs = [];
+ this.mGoodMsgHdrs = [];
+ this.mMessages = {};
+ this.mMessageQueue = [];
+ this.mTotalMessages = aTotalMessages;
+ this.mProcessedMessages = 0;
+ this.firstMessage = true;
+ this.lastStatusTime = Date.now();
+}
+
+/**
+ * @implements {nsIJunkMailClassificationListener}
+ */
+MessageClassifier.prototype = {
+ /**
+ * Starts the message classification process for a message. If the message
+ * sender's address is whitelisted, the message is skipped.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - The header of the message to classify.
+ * @param {nsISpamSettings} aSpamSettings - The object with information about
+ * whitelists
+ */
+ analyzeMessage(aMsgHdr, aSpamSettings) {
+ var junkscoreorigin = aMsgHdr.getStringProperty("junkscoreorigin");
+ if (junkscoreorigin == "user") {
+ // don't override user-set junk status
+ return;
+ }
+
+ // check whitelisting
+ if (aSpamSettings.checkWhiteList(aMsgHdr)) {
+ // message is ham from whitelist
+ var db = aMsgHdr.folder.msgDatabase;
+ db.setStringProperty(
+ aMsgHdr.messageKey,
+ "junkscore",
+ Ci.nsIJunkMailPlugin.IS_HAM_SCORE
+ );
+ db.setStringProperty(aMsgHdr.messageKey, "junkscoreorigin", "whitelist");
+ this.mGoodMsgHdrs.push(aMsgHdr);
+ return;
+ }
+
+ let messageURI = aMsgHdr.folder.generateMessageURI(aMsgHdr.messageKey);
+ this.mMessages[messageURI] = aMsgHdr;
+ if (this.firstMessage) {
+ this.firstMessage = false;
+ MailServices.junk.classifyMessage(messageURI, top.msgWindow, this);
+ } else {
+ this.mMessageQueue.push(messageURI);
+ }
+ },
+
+ /**
+ * Callback function from nsIJunkMailPlugin with classification results.
+ *
+ * @param {string} aClassifiedMsgURI - URI of classified message.
+ * @param {integer} aClassification - Junk classification (0: UNCLASSIFIED, 1: GOOD, 2: JUNK)
+ * @param {integer} aJunkPercent - 0 - 100 indicator of junk likelihood,
+ * with 100 meaning probably junk.
+ * @see {nsIJunkMailClassificationListener}
+ */
+ async onMessageClassified(aClassifiedMsgURI, aClassification, aJunkPercent) {
+ if (!aClassifiedMsgURI) {
+ // Ignore end of batch.
+ return;
+ }
+ var score =
+ aClassification == Ci.nsIJunkMailPlugin.JUNK
+ ? Ci.nsIJunkMailPlugin.IS_SPAM_SCORE
+ : Ci.nsIJunkMailPlugin.IS_HAM_SCORE;
+ const statusDisplayInterval = 1000; // milliseconds between status updates
+
+ // set these props via the db (instead of the message header
+ // directly) so that the nsMsgDBView knows to update the UI
+ //
+ var msgHdr = this.mMessages[aClassifiedMsgURI];
+ var db = msgHdr.folder.msgDatabase;
+ db.setStringProperty(msgHdr.messageKey, "junkscore", score);
+ db.setStringProperty(msgHdr.messageKey, "junkscoreorigin", "plugin");
+ db.setStringProperty(msgHdr.messageKey, "junkpercent", aJunkPercent);
+
+ if (aClassification == Ci.nsIJunkMailPlugin.JUNK) {
+ this.mJunkMsgHdrs.push(msgHdr);
+ } else if (aClassification == Ci.nsIJunkMailPlugin.GOOD) {
+ this.mGoodMsgHdrs.push(msgHdr);
+ }
+
+ var nextMsgURI = this.mMessageQueue.shift();
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+
+ if (nextMsgURI) {
+ ++this.mProcessedMessages;
+ if (Date.now() > this.lastStatusTime + statusDisplayInterval) {
+ this.lastStatusTime = Date.now();
+ var percentDone = 0;
+ if (this.mTotalMessages) {
+ percentDone = Math.round(
+ (this.mProcessedMessages * 100) / this.mTotalMessages
+ );
+ }
+ top.window.MsgStatusFeedback.showStatusString(
+ bundle.formatStringFromName("junkAnalysisPercentComplete", [
+ percentDone + "%",
+ ])
+ );
+ }
+ MailServices.junk.classifyMessage(nextMsgURI, top.msgWindow, this);
+ } else {
+ top.window.MsgStatusFeedback.showStatusString(
+ bundle.GetStringFromName("processingJunkMessages")
+ );
+ await performActionsOnJunkMsgs(
+ this.mFolder,
+ this.mJunkMsgHdrs,
+ this.mGoodMsgHdrs
+ );
+ setTimeout(() => {
+ top.window.MsgStatusFeedback.showStatusString("");
+ }, 500);
+ }
+ },
+};
+
+/**
+ * Filter all messages in the current folder for junk
+ */
+async function filterFolderForJunk() {
+ await processFolderForJunk(true);
+}
+
+/**
+ * Filter selected messages in the current folder for junk
+ */
+async function analyzeMessagesForJunk() {
+ await processFolderForJunk(false);
+}
+
+/**
+ * Filter messages in the current folder for junk
+ *
+ * @param {boolean} aAll - true to filter all messages, else filter selection.
+ */
+async function processFolderForJunk(aAll) {
+ let indices;
+ if (aAll) {
+ // need to expand all threads, so we analyze everything
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var count = treeView.rowCount;
+ if (!count) {
+ return;
+ }
+ } else {
+ indices =
+ AppConstants.MOZ_APP_NAME == "seamonkey"
+ ? window.GetSelectedIndices(gDBView)
+ : window.threadTree?.selectedIndices;
+ if (!indices || !indices.length) {
+ return;
+ }
+ }
+ let totalMessages = aAll ? count : indices.length;
+
+ // retrieve server and its spam settings via the header of an arbitrary message
+ let tmpMsgURI;
+ for (let i = 0; i < totalMessages; i++) {
+ let index = aAll ? i : indices[i];
+ try {
+ tmpMsgURI = gDBView.getURIForViewIndex(index);
+ break;
+ } catch (e) {
+ // dummy headers will fail, so look for another
+ continue;
+ }
+ }
+ if (!tmpMsgURI) {
+ return;
+ }
+
+ let tmpMsgHdr =
+ MailServices.messageServiceFromURI(tmpMsgURI).messageURIToMsgHdr(tmpMsgURI);
+ let spamSettings = tmpMsgHdr.folder.server.spamSettings;
+
+ // create a classifier instance to classify messages in the folder.
+ let msgClassifier = new MessageClassifier(tmpMsgHdr.folder, totalMessages);
+
+ for (let i = 0; i < totalMessages; i++) {
+ let index = aAll ? i : indices[i];
+ try {
+ let msgURI = gDBView.getURIForViewIndex(index);
+ let msgHdr =
+ MailServices.messageServiceFromURI(msgURI).messageURIToMsgHdr(msgURI);
+ msgClassifier.analyzeMessage(msgHdr, spamSettings);
+ } catch (ex) {
+ // blow off errors here - dummy headers will fail
+ }
+ }
+ if (msgClassifier.firstMessage) {
+ // the async plugin was not used, maybe all whitelisted?
+ await performActionsOnJunkMsgs(
+ msgClassifier.mFolder,
+ msgClassifier.mJunkMsgHdrs,
+ msgClassifier.mGoodMsgHdrs
+ );
+ }
+}
+
+/**
+ * Delete junk messages in the current folder. This provides the guarantee that
+ * the method will be synchronous if no messages are deleted.
+ *
+ * @returns {integer} The number of messages deleted.
+ */
+function deleteJunkInFolder() {
+ // use direct folder commands if possible so we don't mess with the selection
+ let selectedFolder = gViewWrapper.displayedFolder;
+ if (!selectedFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ let junkMsgHdrs = [];
+ for (let msgHdr of gDBView.msgFolder.messages) {
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ if (junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE) {
+ junkMsgHdrs.push(msgHdr);
+ }
+ }
+
+ if (junkMsgHdrs.length) {
+ gDBView.msgFolder.deleteMessages(
+ junkMsgHdrs,
+ top.msgWindow,
+ false,
+ false,
+ null,
+ true
+ );
+ }
+ return junkMsgHdrs.length;
+ }
+
+ // Folder is virtual, let the view do the work (but we lose selection)
+
+ // need to expand all threads, so we find everything
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var count = treeView.rowCount;
+ if (!count) {
+ return 0;
+ }
+
+ var treeSelection = treeView.selection;
+
+ var clearedSelection = false;
+
+ // select the junk messages
+ var messageUri;
+ let numMessagesDeleted = 0;
+ for (let i = 0; i < count; ++i) {
+ try {
+ messageUri = gDBView.getURIForViewIndex(i);
+ } catch (ex) {
+ continue; // blow off errors for dummy rows
+ }
+ let msgHdr =
+ MailServices.messageServiceFromURI(messageUri).messageURIToMsgHdr(
+ messageUri
+ );
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ var isJunk = junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE;
+ // if the message is junk, select it.
+ if (isJunk) {
+ // only do this once
+ if (!clearedSelection) {
+ // clear the current selection
+ // since we will be deleting all selected messages
+ treeSelection.clearSelection();
+ clearedSelection = true;
+ treeSelection.selectEventsSuppressed = true;
+ }
+ treeSelection.rangedSelect(i, i, true /* augment */);
+ numMessagesDeleted++;
+ }
+ }
+
+ // if we didn't clear the selection
+ // there was no junk, so bail.
+ if (!clearedSelection) {
+ return 0;
+ }
+
+ treeSelection.selectEventsSuppressed = false;
+ // delete the selected messages
+ //
+ // We'll leave no selection after the delete
+ if ("gNextMessageViewIndexAfterDelete" in window) {
+ window.gNextMessageViewIndexAfterDelete = 0xffffffff; // nsMsgViewIndex_None
+ }
+ gDBView.doCommand(Ci.nsMsgViewCommandType.deleteMsg);
+ treeSelection.clearSelection();
+ return numMessagesDeleted;
+}