Bug 376546 - Wishlist: Message Filters should have "Mark as unread" action r=bienvenu r=IanN
authoraceman <acelists@atlas.sk>
Wed, 23 Nov 2011 15:44:00 +0100
changeset 8938 0fe60a45c85fe52e785d728ab1c0826bc59b087a
parent 8937 f1cec15376bedef7f0a15f3d409863a66dd2c2b9
child 8939 4bbd139f57d30b9ff3875d703ead6b8718f41ea8
push id6854
push userjh@junetz.de
push dateSat, 03 Dec 2011 18:13:55 +0000
treeherdercomm-central@4bbd139f57d3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbienvenu, IanN
bugs376546
Bug 376546 - Wishlist: Message Filters should have "Mark as unread" action r=bienvenu r=IanN
mail/locales/en-US/chrome/messenger/FilterEditor.dtd
mail/locales/en-US/chrome/messenger/filter.properties
mailnews/base/search/content/FilterEditor.js
mailnews/base/search/content/searchWidgets.xml
mailnews/base/search/public/nsMsgFilterCore.idl
mailnews/base/search/src/nsMsgFilter.cpp
mailnews/base/search/src/nsMsgFilterService.cpp
mailnews/imap/src/nsImapMailFolder.cpp
mailnews/imap/test/unit/test_imapFilterActions.js
mailnews/local/src/nsParseMailbox.cpp
mailnews/local/src/nsParseMailbox.h
mailnews/news/src/nsNNTPNewsgroupList.cpp
mailnews/news/test/unit/postings/auto-add/post8.eml
mailnews/news/test/unit/test_filter.js
mailnews/news/test/unit/test_internalUris.js
mailnews/test/resources/filterTestUtils.js
suite/locales/en-US/chrome/mailnews/FilterEditor.dtd
suite/locales/en-US/chrome/mailnews/filter.properties
--- a/mail/locales/en-US/chrome/messenger/FilterEditor.dtd
+++ b/mail/locales/en-US/chrome/messenger/FilterEditor.dtd
@@ -23,16 +23,17 @@
 <!ENTITY filterActionDesc.accesskey "P">
 
 <!-- New Style Filter Rule Actions -->
 <!ENTITY moveMessage.label "Move Message to">
 <!ENTITY copyMessage.label "Copy Message to">
 <!ENTITY forwardTo.label "Forward Message to">
 <!ENTITY replyWithTemplate.label "Reply with Template">
 <!ENTITY markMessageRead.label "Mark As Read">
+<!ENTITY markMessageUnread.label "Mark As Unread">
 <!ENTITY markMessageStarred.label "Add Star">
 <!ENTITY setPriority.label "Set Priority to">
 <!ENTITY addTag.label "Tag Message">
 <!ENTITY setJunkScore.label "Set Junk Status to">
 <!ENTITY deleteMessage.label "Delete Message">
 <!ENTITY deleteFromPOP.label "Delete From POP Server">
 <!ENTITY fetchFromPOP.label "Fetch From POP Server">
 <!ENTITY ignoreThread.label "Ignore Thread">
--- a/mail/locales/en-US/chrome/messenger/filter.properties
+++ b/mail/locales/en-US/chrome/messenger/filter.properties
@@ -47,13 +47,14 @@ filterAction10=forwarded
 filterAction11=execution stopped
 filterAction12=deleted from POP3 server
 filterAction13=left on POP3 server
 filterAction14=junk score
 filterAction15=body fetched from POP3 server
 filterAction16=copied to folder
 filterAction17=tagged
 filterAction18=ignored subthread
+filterAction19=marked as unread
 # LOCALIZATION NOTE(filterAutoNameStr)
 # %1$S=Header or item to match, e.g. "From", "Tag", "Age in days", etc.
 # %2$S=Operator, e.g. "Contains", "is", "is greater than", etc.
 # %3$S=Value, e.g. "Steve Jobs", "Important", "42", etc.
 filterAutoNameStr=%1$S %2$S: %3$S
--- a/mailnews/base/search/content/FilterEditor.js
+++ b/mailnews/base/search/content/FilterEditor.js
@@ -62,17 +62,17 @@ var gFilterActionList;
 var gCustomActions = null;
 var gFilterType;
 
 var gFilterActionStrings = ["none", "movemessage", "setpriorityto", "deletemessage",
                             "markasread", "ignorethread", "watchthread", "markasflagged",
                             "label", "replytomessage", "forwardmessage", "stopexecution",
                             "deletefrompopserver",  "leaveonpopserver", "setjunkscore",
                             "fetchfrompopserver", "copymessage", "addtagtomessage",
-                            "ignoresubthread"];
+                            "ignoresubthread", "markasunread"];
 
 var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction;
 
 var gFilterEditorMsgWindow = null;
 
 function filterEditorOnLoad()
 {
   getCustomActions();
--- a/mailnews/base/search/content/searchWidgets.xml
+++ b/mailnews/base/search/content/searchWidgets.xml
@@ -69,16 +69,17 @@
           <xul:menupopup>
             <xul:menuitem label="&moveMessage.label;" value="movemessage" enablefornews="false"/>
             <xul:menuitem label="&copyMessage.label;" value="copymessage"/>
             <xul:menuseparator enablefornews="false"/>
             <xul:menuitem label="&forwardTo.label;" value="forwardmessage" enablefornews="false"/>
             <xul:menuitem label="&replyWithTemplate.label;" value="replytomessage" enablefornews="false"/>
             <xul:menuseparator/>
             <xul:menuitem label="&markMessageRead.label;" value="markasread"/>
+            <xul:menuitem label="&markMessageUnread.label;" value="markasunread"/>
             <xul:menuitem label="&markMessageStarred.label;" value="markasflagged"/>
             <xul:menuitem label="&setPriority.label;"  value="setpriorityto"/>
             <xul:menuitem label="&addTag.label;"  value="addtagtomessage"/>
             <xul:menuitem label="&setJunkScore.label;" value="setjunkscore" enablefornews="false"/>
             <xul:menuseparator enableforpop3="true"/>
             <xul:menuitem label="&deleteMessage.label;" value="deletemessage"/>
             <xul:menuitem label="&deleteFromPOP.label;" value="deletefrompopserver" enableforpop3="true"/>
             <xul:menuitem label="&fetchFromPOP.label;"  value="fetchfrompopserver" enableforpop3="true"/>
--- a/mailnews/base/search/public/nsMsgFilterCore.idl
+++ b/mailnews/base/search/public/nsMsgFilterCore.idl
@@ -85,10 +85,11 @@ interface nsMsgFilterAction {
     const long StopExecution=11;
     const long DeleteFromPop3Server=12;
     const long LeaveOnPop3Server=13;
     const long JunkScore=14;
     const long FetchBodyFromPop3Server=15;
     const long CopyToFolder=16;
     const long AddTag=17;
     const long KillSubthread=18;
+    const long MarkUnread=19;
 };
 
--- a/mailnews/base/search/src/nsMsgFilter.cpp
+++ b/mailnews/base/search/src/nsMsgFilter.cpp
@@ -907,16 +907,17 @@ static struct RuleActionsTableEntry rule
   { nsMsgFilterAction::Reply,                   "Reply"},
   { nsMsgFilterAction::Forward,                 "Forward"},
   { nsMsgFilterAction::StopExecution,           "Stop execution"},
   { nsMsgFilterAction::DeleteFromPop3Server,    "Delete from Pop3 server"},
   { nsMsgFilterAction::LeaveOnPop3Server,       "Leave on Pop3 server"},
   { nsMsgFilterAction::JunkScore,               "JunkScore"},
   { nsMsgFilterAction::FetchBodyFromPop3Server, "Fetch body from Pop3Server"},
   { nsMsgFilterAction::AddTag,                  "AddTag"},
+  { nsMsgFilterAction::MarkUnread,              "Mark unread"},
   { nsMsgFilterAction::Custom,                  "Custom"},
 };
 
 static const unsigned int sNumActions = NS_ARRAY_LENGTH(ruleActionsTable);
 
 const char *nsMsgFilter::GetActionStr(nsMsgRuleActionType action)
 {
   for (unsigned int i = 0; i < sNumActions; i++)
--- a/mailnews/base/search/src/nsMsgFilterService.cpp
+++ b/mailnews/base/search/src/nsMsgFilterService.cpp
@@ -579,23 +579,25 @@ nsresult nsMsgFilterAfterTheFact::ApplyF
                                                nsMsgProcessingFlags::FilterToMove);
             return rv;
           }
         }
         //we have already moved the hdrs so we can't apply more actions
         if (actionType == nsMsgFilterAction::MoveToFolder)
           *aApplyMore = PR_FALSE;
       }
-
         break;
       case nsMsgFilterAction::MarkRead:
           // crud, no listener support here - we'll probably just need to go on and apply
           // the next filter, and, in the imap case, rely on multiple connection and url
           // queueing to stay out of trouble
-          m_curFolder->MarkMessagesRead(m_searchHitHdrs, PR_TRUE);
+        m_curFolder->MarkMessagesRead(m_searchHitHdrs, true);
+        break;
+      case nsMsgFilterAction::MarkUnread:
+        m_curFolder->MarkMessagesRead(m_searchHitHdrs, false);
         break;
       case nsMsgFilterAction::MarkFlagged:
         m_curFolder->MarkMessagesFlagged(m_searchHitHdrs, PR_TRUE);
         break;
       case nsMsgFilterAction::KillThread:
       case nsMsgFilterAction::WatchThread:
         {
           for (PRUint32 msgIndex = 0; msgIndex < m_searchHits.Length(); msgIndex++)
--- a/mailnews/imap/src/nsImapMailFolder.cpp
+++ b/mailnews/imap/src/nsImapMailFolder.cpp
@@ -3597,19 +3597,26 @@ NS_IMETHODIMP nsImapMailFolder::ApplyFil
             rv = copyService->CopyMessages(this, messageArray, dstFolder,
                                            PR_FALSE, nsnull, msgWindow, PR_FALSE);
             NS_ENSURE_SUCCESS(rv, rv);
           }
         }
         break;
         case nsMsgFilterAction::MarkRead:
         {
-          mDatabase->MarkHdrRead(msgHdr, PR_TRUE, nsnull);
-          StoreImapFlags(kImapMsgSeenFlag, PR_TRUE, &msgKey, 1, nsnull);
-          msgIsNew = PR_FALSE;
+          mDatabase->MarkHdrRead(msgHdr, true, nsnull);
+          StoreImapFlags(kImapMsgSeenFlag, true, &msgKey, 1, nsnull);
+          msgIsNew = false;
+        }
+        break;
+        case nsMsgFilterAction::MarkUnread:
+        {
+          mDatabase->MarkHdrRead(msgHdr, false, nsnull);
+          StoreImapFlags(kImapMsgSeenFlag, false, &msgKey, 1, nsnull);
+          msgIsNew = true;
         }
         break;
         case nsMsgFilterAction::MarkFlagged:
         {
           mDatabase->MarkHdrMarked(msgHdr, PR_TRUE, nsnull);
           StoreImapFlags(kImapMsgFlaggedFlag, PR_TRUE, &msgKey, 1, nsnull);
         }
         break;
--- a/mailnews/imap/test/unit/test_imapFilterActions.js
+++ b/mailnews/imap/test/unit/test_imapFilterActions.js
@@ -93,30 +93,29 @@ const gTestArray =
       do_check_eq(gInboxCount, folderCount(gIMAPInbox));
     }
     gInboxCount = folderCount(gIMAPInbox);
     gSubfolderCount = folderCount(gSubfolder);
     setupTest(gBodyFilter, gAction);
   },
   function MarkRead() {
     gAction.type = Ci.nsMsgFilterAction.MarkRead;
-    gChecks = function checks() {
+    gChecks = function checkMarkRead() {
       testCounts(false, 0, 0, 0);
       do_check_true(gHeader.isRead);
     }
     setupTest(gFilter, gAction);
   },
   function MarkReadBody() {
     gAction.type = Ci.nsMsgFilterAction.MarkRead;
-    gChecks = function checkMarkRead() {
+    gChecks = function checkMarkReadBody() {
       testCounts(false, 0, 0, 0);
       do_check_true(gHeader.isRead);
     }
     setupTest(gBodyFilter, gAction);
-
   },
   function KillThread() {
     gAction.type = Ci.nsMsgFilterAction.KillThread;
     gChecks = function checkKillThread() {
       testCounts(false, 0, 0, 0);
       let thread = db().GetThreadContainingMsgHdr(gHeader);
       do_check_neq(0, thread.flags & Ci.nsMsgMessageFlags.Ignored);
     }
@@ -168,16 +167,32 @@ const gTestArray =
       testCounts(false, 1, 0, 0);
       do_check_eq(gHeader.getStringProperty("junkscore"), "100");
       do_check_eq(gHeader.getStringProperty("junkscoreorigin"), "filter");
     }
     setupTest(gBodyFilter, gAction);
   },
 
   // The remaining tests add new messages
+  function MarkUnread() {
+    gAction.type = Ci.nsMsgFilterAction.MarkUnread;
+    gChecks = function checkMarkUnread() {
+      testCounts(true, 1, 1, 1);
+      do_check_true(!gHeader.isRead);
+    }
+    setupTest(gFilter, gAction);
+  },
+  function MarkUnreadBody() {
+    gAction.type = Ci.nsMsgFilterAction.MarkUnread;
+    gChecks = function checkMarkUnreadBody() {
+      testCounts(true, 1, 1, 1);
+      do_check_true(!gHeader.isRead);
+    }
+    setupTest(gBodyFilter, gAction);
+  },
   function WatchThread() {
     gAction.type = Ci.nsMsgFilterAction.WatchThread;
     gChecks = function checkWatchThread() {
       testCounts(true, 1, 1, 1);
       let thread = db().GetThreadContainingMsgHdr(gHeader);
       do_check_neq(0, thread.flags & Ci.nsMsgMessageFlags.Watched);
     }
     setupTest(gFilter, gAction);
--- a/mailnews/local/src/nsParseMailbox.cpp
+++ b/mailnews/local/src/nsParseMailbox.cpp
@@ -2095,19 +2095,23 @@ NS_IMETHODIMP nsParseNewMailState::Apply
             rv = copyService->CopyMessages(m_downloadFolder, messageArray, dstFolder,
                                            PR_FALSE, nsnull, msgWindow, PR_FALSE);
             NS_ENSURE_SUCCESS(rv, rv);
             m_msgCopiedByFilter = PR_TRUE;
           }
         }
         break;
       case nsMsgFilterAction::MarkRead:
-        msgIsNew = PR_FALSE;
+        msgIsNew = false;
         MarkFilteredMessageRead(msgHdr);
         break;
+      case nsMsgFilterAction::MarkUnread:
+        msgIsNew = true;
+        MarkFilteredMessageUnread(msgHdr);
+        break;
       case nsMsgFilterAction::KillThread:
         msgHdr->SetUint32Property("ProtoThreadFlags", nsMsgMessageFlags::Ignored);
         break;
       case nsMsgFilterAction::KillSubthread:
         msgHdr->OrFlags(nsMsgMessageFlags::Ignored, &newFlags);
         break;
       case nsMsgFilterAction::WatchThread:
         msgHdr->OrFlags(nsMsgMessageFlags::Watched, &newFlags);
@@ -2305,30 +2309,45 @@ nsresult nsParseNewMailState::ApplyForwa
       }
     }
   }
   m_replyTemplateUri.Clear();
   m_msgToForwardOrReply = nsnull;
   return rv;
 }
 
-
-int nsParseNewMailState::MarkFilteredMessageRead(nsIMsgDBHdr *msgHdr)
+void nsParseNewMailState::MarkFilteredMessageRead(nsIMsgDBHdr *msgHdr)
 {
   PRUint32 newFlags;
   if (m_mailDB)
   {
-    m_mailDB->MarkHdrRead(msgHdr, PR_TRUE, nsnull);
+    m_mailDB->MarkHdrRead(msgHdr, true, nsnull);
   }
   else
   {
     msgHdr->OrFlags(nsMsgMessageFlags::Read, &newFlags);
     msgHdr->AndFlags(~nsMsgMessageFlags::New, &newFlags);
   }
-  return 0;
+}
+
+void nsParseNewMailState::MarkFilteredMessageUnread(nsIMsgDBHdr *msgHdr)
+{
+  PRUint32 newFlags;
+  if (m_mailDB)
+  {
+    nsMsgKey msgKey;
+    msgHdr->GetMessageKey(&msgKey);
+    m_mailDB->AddToNewList(msgKey);
+    m_mailDB->MarkHdrRead(msgHdr, false, nsnull);
+  }
+  else
+  {
+    msgHdr->AndFlags(~nsMsgMessageFlags::Read, &newFlags);
+    msgHdr->OrFlags(nsMsgMessageFlags::New, &newFlags);
+  }
 }
 
 nsresult nsParseNewMailState::EndMsgDownload()
 {
   if (m_moveCoalescer)
     m_moveCoalescer->PlaybackMoves();
 
   // need to do this for all folders that had messages filtered into them
--- a/mailnews/local/src/nsParseMailbox.h
+++ b/mailnews/local/src/nsParseMailbox.h
@@ -256,23 +256,24 @@ public:
                              PRUint32 msgOffset);
   nsresult    ApplyForwardAndReplyFilter(nsIMsgWindow *msgWindow);
 
   // this keeps track of how many messages we downloaded that
   // aren't new - e.g., marked read, or moved to an other server.
   PRInt32     m_numNotNewMessages;
 protected:
   virtual nsresult GetTrashFolder(nsIMsgFolder **pTrashFolder);
-  virtual nsresult  MoveIncorporatedMessage(nsIMsgDBHdr *mailHdr,
+  virtual nsresult MoveIncorporatedMessage(nsIMsgDBHdr *mailHdr,
                                           nsIMsgDatabase *sourceDB,
                                           nsIMsgFolder *destIFolder,
                                           nsIMsgFilter *filter,
                                           nsIMsgWindow *msgWindow);
-  virtual int   MarkFilteredMessageRead(nsIMsgDBHdr *msgHdr);
-  void          LogRuleHit(nsIMsgFilter *filter, nsIMsgDBHdr *msgHdr);
+  virtual void     MarkFilteredMessageRead(nsIMsgDBHdr *msgHdr);
+  virtual void     MarkFilteredMessageUnread(nsIMsgDBHdr *msgHdr);
+  void             LogRuleHit(nsIMsgFilter *filter, nsIMsgDBHdr *msgHdr);
 
   nsCOMPtr <nsIMsgFilterList> m_filterList;
   nsCOMPtr <nsIMsgFilterList> m_deferredToServerFilterList;
   nsCOMPtr <nsIMsgFolder> m_rootFolder;
   nsCOMPtr <nsIMsgWindow> m_msgWindow;
   nsCOMPtr <nsIMsgFolder> m_downloadFolder;
   nsCOMArray <nsIMsgFolder> m_filterTargetFolders;
 
--- a/mailnews/news/src/nsNNTPNewsgroupList.cpp
+++ b/mailnews/news/src/nsNNTPNewsgroupList.cpp
@@ -683,17 +683,20 @@ NS_IMETHODIMP nsNNTPNewsgroupList::Apply
     if (NS_SUCCEEDED(filterAction->GetType(&actionType)))
     {
       switch (actionType)
       {
       case nsMsgFilterAction::Delete:
         m_addHdrToDB = PR_FALSE;
         break;
       case nsMsgFilterAction::MarkRead:
-        m_newsDB->MarkHdrRead(m_newMsgHdr, PR_TRUE, nsnull);
+        m_newsDB->MarkHdrRead(m_newMsgHdr, true, nsnull);
+        break;
+      case nsMsgFilterAction::MarkUnread:
+        m_newsDB->MarkHdrRead(m_newMsgHdr, false, nsnull);
         break;
       case nsMsgFilterAction::KillThread:
         m_newMsgHdr->SetUint32Property("ProtoThreadFlags", nsMsgMessageFlags::Ignored);
         break;
       case nsMsgFilterAction::KillSubthread:
         {
           PRUint32 newFlags;
           m_newMsgHdr->OrFlags(nsMsgMessageFlags::Ignored, &newFlags);
new file mode 100644
--- /dev/null
+++ b/mailnews/news/test/unit/postings/auto-add/post8.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Odd/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <8.unread@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:8
+
+This is the eight body post.
--- a/mailnews/news/test/unit/test_filter.js
+++ b/mailnews/news/test/unit/test_filter.js
@@ -23,17 +23,18 @@ load("../../../resources/filterTestUtils
 // These are the expected results for testing filter triggers
 var attribResults = {
   "1@regular.invalid" : ["isRead", false],
   "2@regular.invalid" : ["isRead", true],
   "3@regular.invalid" : ["isRead", true],
   "4@regular.invalid" : ["isRead", true],
   "5@regular.invalid" : ["isRead", true],
   "6.odd@regular.invalid" : ["isRead", true],
-  "7@regular.invalid" : ["isRead", true]
+  "7@regular.invalid" : ["isRead", true],
+  "8.unread@regular.invalid" : ["isRead", true]
 };
 function testAttrib(handler, daemon, localserver) {
   var server = makeServer(handler, daemon);
   server.start(NNTP_PORT);
 
   // Get the folder and force filters to run
   var folder = localserver.rootFolder.getChildNamed("test.filter");
   folder.getNewMessages(null, {
@@ -42,17 +43,17 @@ function testAttrib(handler, daemon, loc
 
   var headerEnum = folder.messages;
   var headers = [];
   while (headerEnum.hasMoreElements())
     headers.push(headerEnum.getNext().QueryInterface(Ci.nsIMsgDBHdr));
 
   try
   {
-    do_check_eq(headers.length, 7);
+    do_check_eq(headers.length, 8);
     for each (var header in headers) {
       var id = header.messageId;
       dump("Testing message "+id+"\n");
       do_check_eq(header[attribResults[id][0]], attribResults[id][1]);
     }
   } catch (e) {
     print(server.playTransaction().them);
     throw e;
@@ -78,17 +79,18 @@ var actionResults = {
     var watched = Ci.nsMsgMessageFlags.Watched;
     // This is checking the thread's watch flag
     return (flags & watched) == watched;
   },
   "5@regular.invalid" : ["isFlagged", true],
   "6.odd@regular.invalid" : ["isRead", false],
   "7@regular.invalid" : function (header, folder) {
     return header.getStringProperty("keywords") == "tag";
-  }
+  },
+  "8.unread@regular.invalid" : ["isRead", false]
 };
 function testAction(handler, daemon, localserver) {
   var server = makeServer(handler, daemon);
   server.start(NNTP_PORT);
 
   // Get the folder and force filters to run
   var folder = localserver.rootFolder.getChildNamed("test.filter");
   folder.getNewMessages(null, {
@@ -97,17 +99,17 @@ function testAction(handler, daemon, loc
 
   var headerEnum = folder.messages;
   var headers = [];
   while (headerEnum.hasMoreElements())
     headers.push(headerEnum.getNext().QueryInterface(Ci.nsIMsgDBHdr));
 
   try
   {
-    do_check_eq(headers.length, 6);
+    do_check_eq(headers.length, 7);
     for each (var header in headers) {
       var id = header.messageId;
       dump("Testing message "+id+"\n");
       if (actionResults[id] instanceof Array)
         do_check_eq(header[actionResults[id][0]], actionResults[id][1]);
       else
         do_check_true(actionResults[id](header, folder));
     }
@@ -132,16 +134,17 @@ function run_test() {
 
   createFilter(serverFilters, "subject", "Odd", "read");
   createFilter(serverFilters, "from", "Odd Person", "read");
   // A PRTime is the time in μs, but a JS date is time in ms.
   createFilter(serverFilters, "date", new Date(2000, 0, 1)*1000, "read");
   createFilter(serverFilters, "size", 2, "read");
   createFilter(serverFilters, "message-id", "odd", "read");
   createFilter(serverFilters, "user-agent", "Odd/1.0", "read");
+  createFilter(serverFilters, "message-id", "8.unread", "read");
   localserver.setFilterList(serverFilters);
 
   handlers.forEach( function (handler) {
     testAttrib(handler, daemon, localserver);
   });
 
   // Now we test folder-filters... and actions
   // Clear out the server filters
@@ -155,15 +158,17 @@ function run_test() {
   createFilter(folderFilters, "subject", "Odd", "delete");
   createFilter(folderFilters, "from", "Odd Person", "kill");
   createFilter(folderFilters, "date", new Date(2000, 0, 1)*1000, "watch");
   createFilter(folderFilters, "size", 2, "flag");
   createFilter(folderFilters, "message-id", "odd", "stop");
   // This shouldn't be hit, because of the previous filter
   createFilter(folderFilters, "message-id", "6.odd", "read");
   createFilter(folderFilters, "user-agent", "Odd/1.0", "tag");
+  createFilter(folderFilters, "message-id", "8.unread", "read");
+  createFilter(folderFilters, "message-id", "8.unread", "unread");
   folderFilters.loggingEnabled = true;
   folder.setFilterList(folderFilters);
 
   handlers.forEach( function (handler) {
     testAction(handler, daemon, localserver);
   });
 }
--- a/mailnews/news/test/unit/test_internalUris.js
+++ b/mailnews/news/test/unit/test_internalUris.js
@@ -41,17 +41,17 @@ let tests = [
 ];
 
 function test_newMsgs() {
   // This tests nsMsgNewsFolder::GetNewsMessages via getNewMessages
   let folder = localserver.rootFolder.getChildNamed("test.filter");
   do_check_eq(folder.getTotalMessages(false), 0);
   folder.getNewMessages(null, asyncUrlListener);
   yield false;
-  do_check_eq(folder.getTotalMessages(false), 7);
+  do_check_eq(folder.getTotalMessages(false), 8);
   yield true;
 }
 
 // Prompts for cancel
 function alert(title, text) {}
 function confirmEx(title, text, flags) {  return 0; }
 
 function test_cancel() {
@@ -72,17 +72,17 @@ function test_cancel() {
       }
     }
   };
   mailSession.AddFolderListener(folderListener, Ci.nsIFolderListener.event);
   folder.QueryInterface(Ci.nsIMsgNewsFolder)
         .cancelMessage(hdr, dummyMsgWindow);
   yield false;
 
-  do_check_eq(folder.getTotalMessages(false), 6);
+  do_check_eq(folder.getTotalMessages(false), 7);
   yield true;
 }
 
 function test_fetchMessage() {
   // Tests nsNntpService::CreateMessageIDURL via FetchMessage
   var statuscode = -1;
   let streamlistener = {
     onDataAvailable: function() {},
--- a/mailnews/test/resources/filterTestUtils.js
+++ b/mailnews/test/resources/filterTestUtils.js
@@ -14,16 +14,17 @@ var ATTRIB_MAP = {
                   "User-Agent"]
 };
 // And this maps strings to filter actions
 var ACTION_MAP = {
   // Template : [action, auxiliary attribute field, auxiliary value]
   "priority" : [Ci.nsMsgFilterAction.ChangePriority, "priority", 6],
   "delete" : [Ci.nsMsgFilterAction.Delete],
   "read" : [Ci.nsMsgFilterAction.MarkRead],
+  "unread" : [Ci.nsMsgFilterAction.MarkUnread],
   "kill" : [Ci.nsMsgFilterAction.KillThread],
   "watch" : [Ci.nsMsgFilterAction.WatchThread],
   "flag" : [Ci.nsMsgFilterAction.MarkFlagged],
   "stop": [Ci.nsMsgFilterAction.StopExecution],
   "tag" : [Ci.nsMsgFilterAction.AddTag, "strValue", "tag"]
 };
 
 /**
--- a/suite/locales/en-US/chrome/mailnews/FilterEditor.dtd
+++ b/suite/locales/en-US/chrome/mailnews/FilterEditor.dtd
@@ -78,16 +78,17 @@
 <!ENTITY filterActionDesc.accesskey "P">
 
 <!-- New Style Filter Rule Actions -->
 <!ENTITY moveMessage.label "Move Message to">
 <!ENTITY copyMessage.label "Copy Message to">
 <!ENTITY forwardTo.label "Forward Message to">
 <!ENTITY replyWithTemplate.label "Reply with Template">
 <!ENTITY markMessageRead.label "Mark As Read">
+<!ENTITY markMessageUnread.label "Mark As Unread">
 <!ENTITY markMessageStarred.label "Mark As Flagged">
 <!ENTITY setPriority.label "Set Priority to">
 <!ENTITY addTag.label "Tag Message">
 <!ENTITY setJunkScore.label "Set Junk Status to">
 <!ENTITY deleteMessage.label "Delete Message">
 <!ENTITY deleteFromPOP.label "Delete From POP Server">
 <!ENTITY fetchFromPOP.label "Fetch From POP Server">
 <!ENTITY ignoreThread.label "Ignore Thread">
--- a/suite/locales/en-US/chrome/mailnews/filter.properties
+++ b/suite/locales/en-US/chrome/mailnews/filter.properties
@@ -46,13 +46,14 @@ filterAction10=forwarded
 filterAction11=execution stopped
 filterAction12=deleted from POP3 server
 filterAction13=left on POP3 server
 filterAction14=junk score
 filterAction15=body fetched from POP3 server
 filterAction16=copied to folder
 filterAction17=tagged
 filterAction18=ignored subthread
+filterAction19=marked as unread
 # LOCALIZATION NOTE(filterAutoNameStr)
 # %1$S=Header or item to match, e.g. "From", "Tag", "Age in days", etc.
 # %2$S=Operator, e.g. "Contains", "is", "is greater than", etc.
 # %3$S=Value, e.g. "Steve Jobs", "Important", "42", etc.
 filterAutoNameStr=%1$S %2$S: %3$S