diff options
Diffstat (limited to 'comm/mailnews/base/content/junkCommands.js')
-rw-r--r-- | comm/mailnews/base/content/junkCommands.js | 449 |
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; +} |