/* 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: * * window.MsgStatusFeedback * * One of: * GetSelectedIndices(view) (in suite) * gFolderDisplay (in mail) * * messenger * gMessengerBundle * gDBView * either gMsgFolderSelected or gFolderDisplay * MsgJunkMailInfo(aCheckFirstUse) * SetNextMessageAfterDelete() * pref * msgWindow */ Components.utils.import("resource:///modules/mailServices.js"); Components.utils.import("resource:///modules/MailUtils.js"); Components.utils.import("resource://gre/modules/Services.jsm"); /* * determineActionsForJunkMsgs * * Determines the actions that should be carried out on the messages * that are being marked as junk * * @param aFolder * the folder with messages being marked as junk * * @return an object with two properties: 'markRead' (boolean) indicating * whether the messages should be marked as read, and 'junkTargetFolder' * (nsIMsgFolder) specifying 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.flags & Components.interfaces.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.getFolderForURI(spamFolderURI); } return actions; } /** * performActionsOnJunkMsgs * * Performs required operations on a list of newly-classified junk messages * * @param aFolder * the folder with messages being marked as junk * * @param aJunkMsgHdrs * nsIArray containing headers (nsIMsgDBHdr) of new junk messages * * @param aGoodMsgHdrs * nsIArray containing headers (nsIMsgDBHdr) of new good messages */ function performActionsOnJunkMsgs(aFolder, aJunkMsgHdrs, aGoodMsgHdrs) { if (aFolder instanceof Components.interfaces.nsIMsgImapMailFolder) // need to update IMAP custom flags { if (aJunkMsgHdrs.length) { var junkMsgKeys = new Array(); for (var i = 0; i < aJunkMsgHdrs.length; i++) junkMsgKeys[i] = aJunkMsgHdrs.queryElementAt(i, Components.interfaces.nsIMsgDBHdr).messageKey; aFolder.storeCustomKeywords(null, "Junk", "NonJunk", junkMsgKeys, junkMsgKeys.length); } if (aGoodMsgHdrs.length) { var goodMsgKeys = new Array(); for (var i = 0; i < aGoodMsgHdrs.length; i++) goodMsgKeys[i] = aGoodMsgHdrs.queryElementAt(i, Components.interfaces.nsIMsgDBHdr).messageKey; aFolder.storeCustomKeywords(null, "NonJunk", "Junk", goodMsgKeys, goodMsgKeys.length); } } if (aJunkMsgHdrs.length) { var actionParams = determineActionsForJunkMsgs(aFolder); if (actionParams.markRead) aFolder.markMessagesRead(aJunkMsgHdrs, true); if (actionParams.junkTargetFolder) MailServices.copy .CopyMessages(aFolder, aJunkMsgHdrs, actionParams.junkTargetFolder, true /* isMove */, null, msgWindow, true /* allow undo */); } } /** * MessageClassifier * * Helper object storing the list of pending messages to process, * and implementing junk processing callback * * @param aFolder * the folder with messages to be analyzed for junk * @param aTotalMessages * Number of messages to process, used for progress report only */ function MessageClassifier(aFolder, aTotalMessages) { this.mFolder = aFolder; this.mJunkMsgHdrs = Components.classes["@mozilla.org/array;1"] .createInstance(Components.interfaces.nsIMutableArray); this.mGoodMsgHdrs = Components.classes["@mozilla.org/array;1"] .createInstance(Components.interfaces.nsIMutableArray); this.mMessages = new Object(); this.mMessageQueue = new Array(); this.mTotalMessages = aTotalMessages; this.mProcessedMessages = 0; this.firstMessage = true; this.lastStatusTime = Date.now(); } MessageClassifier.prototype = { /** * analyzeMessage * * Starts the message classification process for a message. If the message * sender's address is whitelisted, the message is skipped. * * @param aMsgHdr * The header (nsIMsgDBHdr) of the message to classify. * @param aSpamSettings * nsISpamSettings object with information about whitelists */ analyzeMessage: function(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", Components.interfaces.nsIJunkMailPlugin.IS_HAM_SCORE); db.setStringProperty(aMsgHdr.messageKey, "junkscoreorigin", "whitelist"); this.mGoodMsgHdrs.appendElement(aMsgHdr, false); return; } var messageURI = aMsgHdr.folder.generateMessageURI(aMsgHdr.messageKey) + "?fetchCompleteMessage=true"; this.mMessages[messageURI] = aMsgHdr; if (this.firstMessage) { this.firstMessage = false; MailServices.junk.classifyMessage(messageURI, msgWindow, this); } else this.mMessageQueue.push(messageURI); }, /* * nsIJunkMailClassificationListener implementation * onMessageClassified * * Callback function from nsIJunkMailPlugin with classification results * * @param aClassifiedMsgURI * URI of classified message * @param aClassification * Junk classification (0: UNCLASSIFIED, 1: GOOD, 2: JUNK) * @param aJunkPercent * 0 - 100 indicator of junk likelihood, with 100 meaning probably junk */ onMessageClassified: function(aClassifiedMsgURI, aClassification, aJunkPercent) { if (!aClassifiedMsgURI) return; // ignore end of batch var nsIJunkMailPlugin = Components.interfaces.nsIJunkMailPlugin; var score = (aClassification == nsIJunkMailPlugin.JUNK) ? nsIJunkMailPlugin.IS_SPAM_SCORE : 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 == nsIJunkMailPlugin.JUNK) this.mJunkMsgHdrs.appendElement(msgHdr, false); else if (aClassification == nsIJunkMailPlugin.GOOD) this.mGoodMsgHdrs.appendElement(msgHdr, false); var nextMsgURI = this.mMessageQueue.shift(); 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); var percentStr = percentDone + "%"; window.MsgStatusFeedback.showStatusString( document.getElementById("bundle_messenger") .getFormattedString("junkAnalysisPercentComplete", [percentStr])); } MailServices.junk.classifyMessage(nextMsgURI, msgWindow, this); } else { window.MsgStatusFeedback.showStatusString( document.getElementById("bundle_messenger") .getString("processingJunkMessages")); performActionsOnJunkMsgs(this.mFolder, this.mJunkMsgHdrs, this.mGoodMsgHdrs); window.MsgStatusFeedback.showStatusString(""); } } } /* * filterFolderForJunk * * Filter all messages in the current folder for junk */ function filterFolderForJunk() { processFolderForJunk(true); } /* * analyzeMessagesForJunk * * Filter selected messages in the current folder for junk */ function analyzeMessagesForJunk() { processFolderForJunk(false); } /* * processFolderForJunk * * Filter messages in the current folder for junk * * @param aAll: true to filter all messages, else filter selection */ function processFolderForJunk(aAll) { MsgJunkMailInfo(true); if (aAll) { // need to expand all threads, so we analyze everything gDBView.doCommand(nsMsgViewCommandType.expandAll); var treeView = gDBView.QueryInterface(Components.interfaces.nsITreeView); var count = treeView.rowCount; if (!count) return; } else { // suite uses GetSelectedIndices, mail uses gFolderDisplay.selectedMessages var indices = typeof GetSelectedIndices != "undefined" ? GetSelectedIndices(gDBView) : gFolderDisplay.selectedIndices; if (!indices || !indices.length) return; } var totalMessages = aAll ? count : indices.length; // retrieve server and its spam settings via the header of an arbitrary message for (var i = 0; i < totalMessages; i++) { var index = aAll ? i : indices[i]; try { var tmpMsgURI = gDBView.getURIForViewIndex(index); break; } catch (e) { // dummy headers will fail, so look for another continue; } } if (!tmpMsgURI) return; var tmpMsgHdr = messenger.messageServiceFromURI(tmpMsgURI).messageURIToMsgHdr(tmpMsgURI); var spamSettings = tmpMsgHdr.folder.server.spamSettings; // create a classifier instance to classify messages in the folder. var msgClassifier = new MessageClassifier(tmpMsgHdr.folder, totalMessages); for ( i = 0; i < totalMessages; i++) { var index = aAll ? i : indices[i]; try { var msgURI = gDBView.getURIForViewIndex(index); var msgHdr = messenger.messageServiceFromURI(msgURI).messageURIToMsgHdr(msgURI); msgClassifier.analyzeMessage(msgHdr, spamSettings); } catch (ex) { // blow off errors here - dummy headers will fail var msgURI = null; } } if (msgClassifier.firstMessage) // the async plugin was not used, maybe all whitelisted? performActionsOnJunkMsgs(msgClassifier.mFolder, msgClassifier.mJunkMsgHdrs, msgClassifier.mGoodMsgHdrs); } function JunkSelectedMessages(setAsJunk) { MsgJunkMailInfo(true); // When the user explicitly marks a message as junk, we can mark it as read, // too. This is independent of the "markAsReadOnSpam" pref, which applies // only to automatically-classified messages. // Note that this behaviour should match the one in the back end for marking // as junk via clicking the 'junk' column. if (setAsJunk && Services.prefs.getBoolPref("mailnews.ui.junk.manualMarkAsJunkMarksRead")) MarkSelectedMessagesRead(true); gDBView.doCommand(setAsJunk ? nsMsgViewCommandType.junk : nsMsgViewCommandType.unjunk); } /** * Delete junk messages in the current folder. This provides the guarantee that * the method will be synchronous if no messages are deleted. * * @returns The number of messages deleted. */ function deleteJunkInFolder() { MsgJunkMailInfo(true); // use direct folder commands if possible so we don't mess with the selection let selectedFolder = gFolderDisplay.displayedFolder; if ( !(selectedFolder.flags & Components.interfaces.nsMsgFolderFlags.Virtual) ) { var junkMsgHdrs = Components.classes["@mozilla.org/array;1"] .createInstance(Components.interfaces.nsIMutableArray); var enumerator = gDBView.msgFolder.messages; while (enumerator.hasMoreElements()) { var msgHdr = enumerator.getNext().QueryInterface(Components.interfaces.nsIMsgDBHdr); var junkScore = msgHdr.getStringProperty("junkscore"); if (junkScore == Components.interfaces.nsIJunkMailPlugin.IS_SPAM_SCORE) junkMsgHdrs.appendElement(msgHdr, false); } if (junkMsgHdrs.length) gDBView.msgFolder.deleteMessages(junkMsgHdrs, 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(nsMsgViewCommandType.expandAll); var treeView = gDBView.QueryInterface(Components.interfaces.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 (var i = 0; i < count; ++i) { try { messageUri = gDBView.getURIForViewIndex(i); } catch (ex) {continue;} // blow off errors for dummy rows var msgHdr = messenger.messageServiceFromURI(messageUri).messageURIToMsgHdr(messageUri); var junkScore = msgHdr.getStringProperty("junkscore"); var isJunk = (junkScore == Components.interfaces.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 gNextMessageViewIndexAfterDelete = nsMsgViewIndex_None; gDBView.doCommand(nsMsgViewCommandType.deleteMsg); treeSelection.clearSelection(); ClearMessagePane(); return numMessagesDeleted; }