Bug 1312813 - Implement user configurable per feed update interval. r=mkmelin a=jorgk
authoralta88 <alta88@gmail.com>
Sat, 15 Oct 2016 14:09:39 -0400
changeset 47546 8571f68411c759762160b13257eb68626b9b00ea
parent 47483 41590e369c5c6cae8a892fd0238c456ecf9da69a
child 47547 f4a143f3ced4dccd1479e61c3f523e85880a20ca
push id4205
push usermozilla@jorgk.com
push dateMon, 06 Feb 2017 22:04:05 +0000
treeherdertry-comm-central@f611682d7c88 [default view] [failures only]
reviewersmkmelin, jorgk
bugs1312813
Bug 1312813 - Implement user configurable per feed update interval. r=mkmelin a=jorgk
mail/base/content/folderPane.js
mail/base/content/mailContextMenus.js
mail/base/content/mailWindowOverlay.js
mail/base/content/mailWindowOverlay.xul
mail/locales/en-US/chrome/messenger-newsblog/am-newsblog.dtd
mail/locales/en-US/chrome/messenger-newsblog/feed-subscriptions.dtd
mail/locales/en-US/chrome/messenger/messenger.dtd
mail/themes/linux/mail/folderPane.css
mail/themes/linux/mail/newsblog/feed-subscriptions.css
mail/themes/osx/mail/folderPane.css
mail/themes/osx/mail/newsblog/feed-subscriptions.css
mail/themes/windows/mail/folderPane.css
mail/themes/windows/mail/newsblog/feed-subscriptions.css
mailnews/extensions/newsblog/content/Feed.js
mailnews/extensions/newsblog/content/FeedItem.js
mailnews/extensions/newsblog/content/FeedUtils.jsm
mailnews/extensions/newsblog/content/am-newsblog.js
mailnews/extensions/newsblog/content/am-newsblog.xul
mailnews/extensions/newsblog/content/feed-parser.js
mailnews/extensions/newsblog/content/feed-subscriptions.js
mailnews/extensions/newsblog/content/feed-subscriptions.xul
mailnews/extensions/newsblog/content/newsblogOverlay.js
mailnews/extensions/newsblog/jar.mn
suite/locales/en-US/chrome/mailnews/newsblog/am-newsblog.dtd
suite/locales/en-US/chrome/mailnews/newsblog/feed-subscriptions.dtd
--- a/mail/base/content/folderPane.js
+++ b/mail/base/content/folderPane.js
@@ -914,16 +914,20 @@ var gFolderTreeView = {
     if (aCol.id != "folderNameCol")
       return "";
 
     let rowItem = gFolderTreeView._rowMap[aRow];
     let folder = rowItem._folder;
     if (folder.server.type != "rss" || folder.isServer)
       return "";
 
+    let properties = this.getFolderCacheProperty(folder, "properties");
+    if (properties.includes("hasError") || properties.includes("isBusy"))
+      return "";
+
     let favicon = this.getFolderCacheProperty(folder, "favicon");
     if (favicon != null)
       return favicon;
 
     let callback = (iconUrl => {
       this.setFolderCacheProperty(folder, "favicon", iconUrl);
       this._tree.invalidateRow(aRow);
     });
@@ -1489,17 +1493,18 @@ var gFolderTreeView = {
    * @param  nsIMsgFolder aFolder   - folder.
    * @param  string aProperty       - property key.
    * @return value or null          - null indicates uninitialized.
    */
   getFolderCacheProperty: function(aFolder, aProperty) {
     if (!aFolder || !aProperty)
       return null;
 
-    if (!(aFolder.URI in this._cache))
+    if (!(aFolder.URI in this._cache) ||
+        !(aProperty in this._cache[aFolder.URI]))
       return null;
 
     return this._cache[aFolder.URI][aProperty];
   },
 
   /**
    * Contains the set of modes registered with the folder tree, initially those
    * included by default. This is a map from names of modes to their
@@ -2377,19 +2382,20 @@ ftvItem.prototype = {
       properties += " specialFolder-" + this._folder.name.replace(' ','');
     }
     // if there is a smartFolder name property, add it
     let smartFolderName = getSmartFolderName(this._folder);
     if (smartFolderName) {
       properties += " specialFolder-" + smartFolderName.replace(' ','');
     }
 
-    if (this._folder.server.type == "rss" && !this._folder.isServer &&
-        FeedUtils.getFeedUrlsInFolder(this._folder))
-      properties += " isFeedFolder-true";
+    if (FeedMessageHandler.isFeedFolder(this._folder)) {
+      properties += FeedUtils.getFolderProperties(this._folder, null);
+      gFolderTreeView.setFolderCacheProperty(this._folder, "properties", properties);
+    }
 
     return properties;
   },
 
   command: function fti_command() {
     if (!Services.prefs.getBoolPref("mailnews.reuse_thread_window2"))
       MsgOpenNewWindowForFolder(this._folder.URI, -1 /* key */);
   },
--- a/mail/base/content/mailContextMenus.js
+++ b/mail/base/content/mailContextMenus.js
@@ -411,16 +411,45 @@ function fillFolderPaneContextMenu(aEven
   // but it gets messy for situations where both server and a folder
   // on the server are selected.
   ShowMenuItem("folderPaneContext-getMessages",
                (selectedServers.length > 0 &&
                 selectedFoldersThatCanGetMessages.length == numSelected &&
                 selectedServers.length == selectedFoldersThatCanGetMessages.length) ||
                selectedFoldersThatCanGetMessages.length == numSelected);
 
+  // --- Set up the pause all updates menu item.
+  // Show only if a feed server.
+  let showPausedAll = numSelected == 1 && folders[0].isServer &&
+                      FeedMessageHandler.isFeedFolder(folders[0]);
+  ShowMenuItem("folderPaneContext-pauseAllUpdates", showPausedAll);
+  // Adjust the checked state on the menu item.
+  if (showPausedAll) {
+    let menuitem = document.getElementById("folderPaneContext-pauseAllUpdates");
+    let optionsAcct = FeedUtils.getOptionsAcct(folders[0].server);
+    menuitem.setAttribute("checked", !optionsAcct.doBiff);
+  }
+
+  // --- Set up the pause single folder subscription updates menu item.
+  // Show only if a feed folder, or a feed account folder with no subscriptions
+  // but subfolders (which may have feed folders).
+  let showPaused = numSelected == 1 &&
+                   (FeedUtils.getFeedUrlsInFolder(folders[0]) ||
+                    (FeedMessageHandler.isFeedFolder(folders[0]) &&
+                     !folders[0].isServer && folders[0].hasSubFolders));
+  ShowMenuItem("folderPaneContext-pauseUpdates", showPaused);
+  // Adjust the checked state on the menu item.
+  if (showPaused) {
+    let menuitem = document.getElementById("folderPaneContext-pauseUpdates");
+    let properties = FeedUtils.getFolderProperties(folders[0]);
+    let paused = properties.includes("isPaused");
+    menuitem.setAttribute("checked", paused);
+  }
+  ShowMenuItem("folderPaneContext-sepPause", showPausedAll || showPaused);
+
   // --- Set up new sub/folder menu item.
   if (numSelected == 1) {
     let showNewFolderItem =
       ((folders[0].server.type != "nntp") && folders[0].canCreateSubfolders) ||
       (specialFolder == "Inbox");
     ShowMenuItem("folderPaneContext-new", showNewFolderItem);
     // XXX: Can't offline imap create folders nowadays?
     EnableMenuItem("folderPaneContext-new", folders[0].server.type != "imap" ||
--- a/mail/base/content/mailWindowOverlay.js
+++ b/mail/base/content/mailWindowOverlay.js
@@ -1434,16 +1434,29 @@ function GetMessagesForInboxOnServer(ser
 
 function MsgGetMessage()
 {
   // if offline, prompt for getting messages
   if (MailOfflineMgr.isOnline() || MailOfflineMgr.getNewMail())
     GetFolderMessages();
 }
 
+function MsgPauseUpdates(aMenuitem)
+{
+  // Pause single feed folder subscription updates, or all account updates if
+  // folder is the account folder.
+  let selectedFolders = GetSelectedMsgFolders();
+  let folder = selectedFolders.length ? selectedFolders[0] : null;
+  if (!FeedMessageHandler.isFeedFolder(folder))
+    return;
+
+  let pause = aMenuitem.getAttribute("checked") == "true";
+  FeedUtils.pauseFeedFolderUpdates(folder, pause, true);
+}
+
 function MsgGetMessagesForAllServers(defaultServer)
 {
   // now log into any server
   try
   {
     var allServers = accountManager.allServers;
     // Array of arrays of servers for a particular folder.
     var pop3DownloadServersArray = [];
--- a/mail/base/content/mailWindowOverlay.xul
+++ b/mail/base/content/mailWindowOverlay.xul
@@ -935,16 +935,27 @@
 
   <menupopup id="folderPaneContext"
              onpopupshowing="return fillFolderPaneContextMenu(event);"
              onpopuphiding="if (event.target == this) folderPaneOnPopupHiding();">
     <menuitem id="folderPaneContext-getMessages"
               label="&folderContextGetMessages.label;"
               accesskey="&folderContextGetMessages.accesskey;"
               oncommand="MsgGetMessage();"/>
+    <menuitem id="folderPaneContext-pauseAllUpdates"
+              type="checkbox"
+              label="&folderContextPauseAllUpdates.label;"
+              accesskey="&folderContextPauseUpdates.accesskey;"
+              oncommand="MsgPauseUpdates(this);"/>
+    <menuitem id="folderPaneContext-pauseUpdates"
+              type="checkbox"
+              label="&folderContextPauseUpdates.label;"
+              accesskey="&folderContextPauseUpdates.accesskey;"
+              oncommand="MsgPauseUpdates(this);"/>
+    <menuseparator id="folderPaneContext-sepPause"/>
     <menuitem id="folderPaneContext-openNewTab"
               label="&folderContextOpenNewTab.label;"
               accesskey="&folderContextOpenNewTab.accesskey;"
               oncommand="FolderPaneContextMenuNewTab(event);"/>
     <menuitem id="folderPaneContext-openNewWindow"
               label="&folderContextOpenInNewWindow.label;"
               accesskey="&folderContextOpenInNewWindow.accesskey;"
               oncommand="MsgOpenNewWindowForFolder(null,-1);"/>
--- a/mail/locales/en-US/chrome/messenger-newsblog/am-newsblog.dtd
+++ b/mail/locales/en-US/chrome/messenger-newsblog/am-newsblog.dtd
@@ -1,24 +1,16 @@
 <!-- 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/. -->
 
-<!ENTITY biffStart.label "Check for new articles every ">
-<!ENTITY biffStart.accesskey "k">
-<!ENTITY biffEnd.label "minutes">
+<!ENTITY loginAtStartup.label          "Check for new articles at startup">
+<!ENTITY loginAtStartup.accesskey      "C">
+<!ENTITY biffAll.label                 "Enable updates for all feeds">
+<!ENTITY biffAll.accesskey             "E">
 
-<!ENTITY loginAtStartup.label "Check for new articles at startup"> 
-<!ENTITY loginAtStartup.accesskey "C">
+<!ENTITY newFeedSettings.label         "Default Settings for New Feeds">
 
-<!ENTITY useQuickMode.label "By default, show the article summary instead of loading the web page"> 
-<!ENTITY useQuickMode.accesskey "B">
-
-<!ENTITY manageSubscriptions.label "Manage Subscriptions…"> 
+<!ENTITY manageSubscriptions.label     "Manage Subscriptions…">
 <!ENTITY manageSubscriptions.accesskey "M">
 
-<!ENTITY feedWindowTitle.label "Feed Account Wizard">
-
-<!-- entities from rss.rdf -->
-<!ENTITY feeds.accountName "Blogs &amp; News Feeds">
-<!ENTITY feeds.wizardShortName "Feeds">
-<!ENTITY feeds.wizardLongName "Blogs &amp; News Feeds">
-<!ENTITY feeds.wizardLongName.accesskey "F">
+<!ENTITY feedWindowTitle.label         "Feed Account Wizard">
+<!ENTITY feeds.accountName             "Blogs &amp; News Feeds">
--- a/mail/locales/en-US/chrome/messenger-newsblog/feed-subscriptions.dtd
+++ b/mail/locales/en-US/chrome/messenger-newsblog/feed-subscriptions.dtd
@@ -5,33 +5,43 @@
 <!-- Subscription Dialog -->
 <!ENTITY feedSubscriptions.label     "Feed Subscriptions">
 <!ENTITY learnMore.label             "Learn more about Feeds">
 
 <!ENTITY feedTitle.label             "Title:">
 <!ENTITY feedTitle.accesskey         "T">
 
 <!ENTITY feedLocation.label          "Feed URL:">
-<!ENTITY feedLocation.accesskey      "U">
+<!ENTITY feedLocation.accesskey      "F">
 <!ENTITY feedLocation.placeholder    "Enter a valid feed url to Add">
 <!ENTITY locationValidate.label      "Validate">
 <!ENTITY validateText.label          "Check validation and retrieve a valid url.">
 
 <!ENTITY feedFolder.label            "Store Articles in:">
 <!ENTITY feedFolder.accesskey        "S">
 
+<!-- Account Settings and Subscription Dialog -->
+<!ENTITY biffStart.label             "Check for new articles every ">
+<!ENTITY biffStart.accesskey         "k">
+<!ENTITY biffMinutes.label           "minutes">
+<!ENTITY biffMinutes.accesskey       "n">
+<!ENTITY biffDays.label              "days">
+<!ENTITY biffDays.accesskey          "d">
+<!ENTITY recommendedUnits.label      "Publisher recommends:">
+
 <!ENTITY quickMode.label             "Show the article summary instead of loading the web page">
 <!ENTITY quickMode.accesskey         "h">
 
 <!ENTITY autotagEnable.label         "Automatically create tags from feed &lt;category&gt; names">
-<!ENTITY autotagEnable.accesskey     "c">
+<!ENTITY autotagEnable.accesskey     "o">
 <!ENTITY autotagUsePrefix.label      "Prefix tags with:">
 <!ENTITY autotagUsePrefix.accesskey  "P">
 <!ENTITY autoTagPrefix.placeholder   "Enter a tag prefix">
 
+<!-- Subscription Dialog -->
 <!ENTITY button.addFeed.label        "Add">
 <!ENTITY button.addFeed.accesskey    "A">
 <!ENTITY button.updateFeed.label     "Update">
 <!ENTITY button.updateFeed.accesskey "U">
 <!ENTITY button.removeFeed.label     "Remove">
 <!ENTITY button.removeFeed.accesskey "R">
 <!ENTITY button.importOPML.label     "Import">
 <!ENTITY button.importOPML.accesskey "I">
--- a/mail/locales/en-US/chrome/messenger/messenger.dtd
+++ b/mail/locales/en-US/chrome/messenger/messenger.dtd
@@ -629,16 +629,19 @@
 <!ENTITY folderNameColumn.label "Name">
 <!ENTITY folderUnreadColumn.label "Unread">
 <!ENTITY folderTotalColumn.label "Total">
 <!ENTITY folderSizeColumn.label "Size">
 
 <!-- Folder Pane Context Menu -->
 <!ENTITY folderContextGetMessages.label "Get Messages">
 <!ENTITY folderContextGetMessages.accesskey "G">
+<!ENTITY folderContextPauseAllUpdates.label "Pause All Updates">
+<!ENTITY folderContextPauseUpdates.label "Pause Updates">
+<!ENTITY folderContextPauseUpdates.accesskey "U">
 <!ENTITY folderContextOpenInNewWindow.label "Open in New Window">
 <!ENTITY folderContextOpenInNewWindow.accesskey "O">
 <!ENTITY folderContextOpenNewTab.label "Open in New Tab">
 <!ENTITY folderContextOpenNewTab.accesskey "T">
 <!ENTITY folderContextNew.label "New Subfolder…">
 <!ENTITY folderContextNew.accesskey "N">
 <!ENTITY folderContextRename.label "Rename">
 <!ENTITY folderContextRename.accesskey "R">
--- a/mail/themes/linux/mail/folderPane.css
+++ b/mail/themes/linux/mail/folderPane.css
@@ -22,21 +22,16 @@ treechildren::-moz-tree-image(folderName
 /* ..... Shared folders .....
 treechildren::-moz-tree-image(folderNameCol, imapShared-true) {
 } */
 
 treechildren::-moz-tree-image(folderNameCol, newMessages-true) {
   list-style-image: url("moz-icon://stock/gtk-directory?size=menu");
 }
 
-treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true, newMessages-true) {
-  list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
-  -moz-image-region: rect(0 16px 16px 0);
-}
-
 /* ..... Inbox ..... */
 
 .tabmail-tab[type="folder"][SpecialFolder="Inbox"],
 treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox) {
   list-style-image: url("chrome://messenger/skin/icons/folder-pane.png");
   -moz-image-region: rect(64px 16px 80px 0px);
 }
 
@@ -109,23 +104,49 @@ treechildren::-moz-tree-image(folderName
 .tabmail-tab[type="folder"][ServerType="nntp"],
 treechildren::-moz-tree-image(folderNameCol, serverType-nntp) {
   list-style-image: url("chrome://messenger/skin/icons/folder-pane.png");
   -moz-image-region: rect(208px 16px 224px 0px);
 }
 
 /* ..... Feed ..... */
 
+treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
+  list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
+  -moz-image-region: rect(32px 16px 48px 0);
+  width: 16px;
+  height: 16px;
+}
 .tabmail-tab[type="folder"][IsFeedFolder="true"],
 treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true) {
   list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
   -moz-image-region: rect(0 16px 16px 0);
   width: 16px;
   height: 16px;
 }
+treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true, newMessages-true) {
+  list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
+  -moz-image-region: rect(0 16px 16px 0);
+}
+treechildren::-moz-tree-image(folderNameCol, serverIsPaused),
+treechildren::-moz-tree-cell-text(folderNameCol, serverIsPaused),
+treechildren::-moz-tree-image(folderNameCol, isPaused),
+treechildren::-moz-tree-cell-text(folderNameCol, isPaused) {
+  opacity: 0.4;
+}
+treechildren::-moz-tree-image(folderNameCol, closed, isBusy),
+treechildren::-moz-tree-image(folderNameCol, leaf, isBusy) {
+  list-style-image: url("chrome://messenger/skin/icons/status.png");
+  -moz-image-region: rect(0 32px 16px 16px);
+}
+treechildren::-moz-tree-image(folderNameCol, closed, hasError),
+treechildren::-moz-tree-image(folderNameCol, leaf, hasError) {
+  list-style-image: url("chrome://global/skin/icons/error-16.png");
+  -moz-image-region: rect(0 16px 16px 0);
+}
 
 treechildren::-moz-tree-cell-text(folderNameCol, newMessages-true),
 treechildren::-moz-tree-cell-text(folderNameCol, specialFolder-Inbox, newMessages-true) {
   font-weight: bold;
 }
 
 /* ..... Mail server  ..... */
 
--- a/mail/themes/linux/mail/newsblog/feed-subscriptions.css
+++ b/mail/themes/linux/mail/newsblog/feed-subscriptions.css
@@ -32,15 +32,8 @@
 }
 
 #nameValue,
 #locationValue,
 #selectFolderValue {
   padding-top: 0;
   padding-bottom: 1px;
 }
-
-treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
-  list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
-  -moz-image-region: rect(32px 16px 48px 0);
-  width: 16px;
-  height: 16px;
-}
--- a/mail/themes/osx/mail/folderPane.css
+++ b/mail/themes/osx/mail/folderPane.css
@@ -27,23 +27,45 @@ treechildren::-moz-tree-image(folderName
 
 /* ..... News Folders ..... */
 .tabmail-tab[type="folder"][ServerType="nntp"],
 treechildren::-moz-tree-image(folderNameCol, serverType-nntp) {
   -moz-image-region: rect(0 160px 16px 144px);
 }
 
 /* ..... Feed Folders ..... */
+treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
+  list-style-image: url("chrome://messenger-newsblog/skin/rss-feed.png");
+  -moz-image-region: rect(0 16px 16px 0);
+  width: 16px;
+  height: 16px;
+}
 .tabmail-tab[type="folder"][IsFeedFolder="true"],
 treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true) {
   list-style-image: url("chrome://messenger/skin/icons/folder-pane.png");
   -moz-image-region: rect(0 224px 16px 208px);
   width: 16px;
   height: 16px;
 }
+treechildren::-moz-tree-image(folderNameCol, serverIsPaused),
+treechildren::-moz-tree-cell-text(folderNameCol, serverIsPaused),
+treechildren::-moz-tree-image(folderNameCol, isPaused),
+treechildren::-moz-tree-cell-text(folderNameCol, isPaused) {
+  opacity: 0.4;
+}
+treechildren::-moz-tree-image(folderNameCol, closed, isBusy),
+treechildren::-moz-tree-image(folderNameCol, leaf, isBusy) {
+  list-style-image: url("chrome://messenger/skin/icons/status.png");
+  -moz-image-region: rect(0 32px 16px 16px);
+}
+treechildren::-moz-tree-image(folderNameCol, closed, hasError),
+treechildren::-moz-tree-image(folderNameCol, leaf, hasError) {
+  list-style-image: url("chrome://global/skin/icons/error-16.png");
+  -moz-image-region: rect(0 16px 16px 0);
+}
 
 /* ..... Inbox ..... */
 .tabmail-tab[type="folder"][SpecialFolder="Inbox"],
 treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox),
 treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox, newMessages-true) {
   list-style-image: url("chrome://messenger/skin/icons/folder-pane.png");
   -moz-image-region: rect(0 48px 16px 32px);
 }
@@ -123,21 +145,35 @@ treechildren::-moz-tree-image(folderName
 
   /* ..... News Folders ..... */
   .tabmail-tab[type="folder"][ServerType="nntp"],
   treechildren::-moz-tree-image(folderNameCol, serverType-nntp) {
     -moz-image-region: rect(0 320px 32px 288px);
   }
 
   /* ..... Feed Folders ..... */
+  treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
+    list-style-image: url("chrome://messenger-newsblog/skin/rss-feed@2x.png");
+    -moz-image-region: rect(0 32px 32px 0);
+  }
   .tabmail-tab[type="folder"][IsFeedFolder="true"],
   treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true) {
     list-style-image: url("chrome://messenger/skin/icons/folder-pane@2x.png");
     -moz-image-region: rect(0 448px 32px 416px);
   }
+  treechildren::-moz-tree-image(folderNameCol, closed, isBusy),
+  treechildren::-moz-tree-image(folderNameCol, leaf, isBusy) {
+    list-style-image: url("chrome://messenger/skin/icons/status@2x.png");
+    -moz-image-region: rect(0 64px 32px 32px);
+  }
+  treechildren::-moz-tree-image(folderNameCol, closed, hasError),
+  treechildren::-moz-tree-image(folderNameCol, leaf, hasError) {
+    list-style-image: url("chrome://global/skin/icons/error.png");
+    -moz-image-region: rect(0 16px 16px 0);
+  }
 
   /* ..... Inbox ..... */
   .tabmail-tab[type="folder"][SpecialFolder="Inbox"],
   treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox),
   treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox, newMessages-true) {
     list-style-image: url("chrome://messenger/skin/icons/folder-pane@2x.png");
     -moz-image-region: rect(0 96px 32px 64px);
   }
--- a/mail/themes/osx/mail/newsblog/feed-subscriptions.css
+++ b/mail/themes/osx/mail/newsblog/feed-subscriptions.css
@@ -20,22 +20,8 @@
 
 #statusContainerBox {
   height: 2.3em;
 }
 
 #autotagPrefix {
   width: 35ch;
 }
-
-treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
-  list-style-image: url("chrome://messenger-newsblog/skin/rss-feed.png");
-  -moz-image-region: rect(0 16px 16px 0);
-  width: 16px;
-  height: 16px;
-}
-
-@media (min-resolution: 2dppx) {
-  treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
-    list-style-image: url("chrome://messenger-newsblog/skin/rss-feed@2x.png");
-    -moz-image-region: rect(0 32px 32px 0);
-  }
-}
--- a/mail/themes/windows/mail/folderPane.css
+++ b/mail/themes/windows/mail/folderPane.css
@@ -39,28 +39,49 @@ treechildren::-moz-tree-image(folderName
 .tabmail-tab[type="folder"][ServerType="nntp"],
 treechildren::-moz-tree-image(folderNameCol, serverType-nntp),
 treechildren::-moz-tree-image(folderNameCol, serverType-nntp, open) {
   -moz-image-region: rect(0 160px 16px 144px);
 }
 
 /* ..... Feed ..... */
 
+treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
+  list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
+  -moz-image-region: rect(32px 16px 48px 0);
+  width: 16px;
+  height: 16px;
+}
 .tabmail-tab[type="folder"][IsFeedFolder="true"],
 treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true),
 treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true, open) {
   list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
   -moz-image-region: rect(0 16px 16px 0);
   width: 16px;
   height: 16px;
 }
-
 treechildren::-moz-tree-image(folderNameCol, isFeedFolder-true, newMessages-true) {
   -moz-image-region: rect(16px 16px 32px 0);
 }
+treechildren::-moz-tree-image(folderNameCol, serverIsPaused),
+treechildren::-moz-tree-cell-text(folderNameCol, serverIsPaused),
+treechildren::-moz-tree-image(folderNameCol, isPaused),
+treechildren::-moz-tree-cell-text(folderNameCol, isPaused) {
+  opacity: 0.4;
+}
+treechildren::-moz-tree-image(folderNameCol, closed, isBusy),
+treechildren::-moz-tree-image(folderNameCol, leaf, isBusy) {
+  list-style-image: url("chrome://messenger/skin/icons/status.png");
+  -moz-image-region: rect(0 32px 16px 16px);
+}
+treechildren::-moz-tree-image(folderNameCol, closed, hasError),
+treechildren::-moz-tree-image(folderNameCol, leaf, hasError) {
+  list-style-image: url("chrome://global/skin/icons/error-16.png");
+  -moz-image-region: rect(0 16px 16px 0);
+}
 
 /* ..... Inbox ..... */
 
 .tabmail-tab[type="folder"][SpecialFolder="Inbox"],
 treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox),
 treechildren::-moz-tree-image(folderNameCol, specialFolder-Inbox, open) {
   -moz-image-region: rect(0 48px 16px 32px);
 }
--- a/mail/themes/windows/mail/newsblog/feed-subscriptions.css
+++ b/mail/themes/windows/mail/newsblog/feed-subscriptions.css
@@ -39,15 +39,8 @@
 }
 
 #nameValue,
 #locationValue,
 #selectFolderValue {
   padding-top: 0;
   padding-bottom: 1px;
 }
-
-treechildren::-moz-tree-image(folderNameCol, isFeed-true) {
-  list-style-image: url("chrome://messenger-newsblog/skin/icons/rss-feed.png");
-  -moz-image-region: rect(32px 16px 48px 0);
-  width: 16px;
-  height: 16px;
-}
--- a/mailnews/extensions/newsblog/content/Feed.js
+++ b/mailnews/extensions/newsblog/content/Feed.js
@@ -47,17 +47,17 @@ var FeedCache =
 };
 
 function Feed(aResource, aRSSServer)
 {
   this.resource = aResource.QueryInterface(Ci.nsIRDFResource);
   this.server = aRSSServer;
 }
 
-Feed.prototype = 
+Feed.prototype =
 {
   description: null,
   author: null,
   request: null,
   server: null,
   downloadCallback: null,
   resource: null,
   items: new Array(),
@@ -194,34 +194,34 @@ Feed.prototype =
     // refresh cycle (i.e. not if the request is cancelled).
     let lastModifiedHeader = request.getResponseHeader("Last-Modified");
     feed.mLastModified = (lastModifiedHeader && feed.parseItems) ?
                            lastModifiedHeader : null;
 
     // The download callback is called asynchronously when parse() is done.
     feed.parse();
   },
-  
-  onProgress: function(aEvent) 
+
+  onProgress: function(aEvent)
   {
     let request = aEvent.target;
     let url = request.channel.originalURI.spec;
     let feed = FeedCache.getFeed(url);
 
     if (feed.downloadCallback)
       feed.downloadCallback.onProgress(feed, aEvent.loaded, aEvent.total,
                                        aEvent.lengthComputable);
   },
 
   onDownloadError: function(aEvent)
   {
     let request = aEvent.target;
     let url = request.channel.originalURI.spec;
     let feed = FeedCache.getFeed(url);
-    if (feed.downloadCallback) 
+    if (feed.downloadCallback)
     {
       // Generic network or 'not found' error initially.
       let error = FeedUtils.kNewsBlogRequestFailure;
 
       if (request.status == 304) {
         // If the http status code is 304, the feed has not been modified
         // since we last downloaded it and does not need to be parsed.
         error = FeedUtils.kNewsBlogNoNewItems;
@@ -345,59 +345,53 @@ Feed.prototype =
 
     return quickMode;
   },
 
   set quickMode (aNewQuickMode)
   {
     let ds = FeedUtils.getSubscriptionsDS(this.server);
     aNewQuickMode = FeedUtils.rdf.GetLiteral(aNewQuickMode);
-    let old_quickMode = ds.GetTarget(this.resource, 
+    let old_quickMode = ds.GetTarget(this.resource,
                                      FeedUtils.FZ_QUICKMODE,
                                      true);
     if (old_quickMode)
       ds.Change(this.resource, FeedUtils.FZ_QUICKMODE,
                 old_quickMode, aNewQuickMode);
     else
       ds.Assert(this.resource, FeedUtils.FZ_QUICKMODE,
                 aNewQuickMode, true);
   },
 
   get options ()
   {
     let ds = FeedUtils.getSubscriptionsDS(this.server);
     let options = ds.GetTarget(this.resource, FeedUtils.FZ_OPTIONS, true);
-    if (options)
-      return JSON.parse(options.QueryInterface(Ci.nsIRDFLiteral).Value);
+    options = options ? JSON.parse(options.QueryInterface(Ci.nsIRDFLiteral).Value) :
+                        null;
+    if (options && options.version == FeedUtils._optionsDefault.version)
+      return options;
 
-    return null;
+    let newOptions = FeedUtils.newOptions(options);
+    this.options = newOptions;
+    return newOptions;
   },
 
   set options (aOptions)
   {
-    let newOptions = aOptions ? FeedUtils.newOptions(aOptions) :
-                                FeedUtils._optionsDefault;
+    let newOptions = aOptions ? aOptions : FeedUtils.optionsTemplate;
     let ds = FeedUtils.getSubscriptionsDS(this.server);
     newOptions = FeedUtils.rdf.GetLiteral(JSON.stringify(newOptions));
     let oldOptions = ds.GetTarget(this.resource, FeedUtils.FZ_OPTIONS, true);
     if (oldOptions)
       ds.Change(this.resource, FeedUtils.FZ_OPTIONS, oldOptions, newOptions);
     else
       ds.Assert(this.resource, FeedUtils.FZ_OPTIONS, newOptions, true);
   },
 
-  categoryPrefs: function ()
-  {
-    let categoryPrefsAcct = FeedUtils.getOptionsAcct(this.server).category;
-    if (!this.options)
-      return categoryPrefsAcct;
-
-    return this.options.category;
-  },
-
   get link ()
   {
     let ds = FeedUtils.getSubscriptionsDS(this.server);
     let link = ds.GetTarget(this.resource, FeedUtils.RSS_LINK, true);
     if (link)
       link = link.QueryInterface(Ci.nsIRDFLiteral).Value;
 
     return link;
@@ -600,17 +594,16 @@ Feed.prototype =
       FeedUtils.log.debug("Feed.cleanupParsingState: items stored - " + this.itemsStored);
     }
 
     // Force the xml http request to go away.  This helps reduce some nasty
     // assertions on shut down.
     this.request = null;
     this.itemsToStore = "";
     this.itemsToStoreIndex = 0;
-    this.itemsStored = 0;
     this.storeItemsTimer = null;
 
     if (aFeed.downloadCallback)
       aFeed.downloadCallback.downloaded(aFeed, aCode);
   },
 
   // nsITimerCallback
   notify: function(aTimer)
--- a/mailnews/extensions/newsblog/content/FeedItem.js
+++ b/mailnews/extensions/newsblog/content/FeedItem.js
@@ -209,17 +209,17 @@ FeedItem.prototype =
                          FeedUtils.rdf.GetResource(this.feed.url), true))
       ds.Assert(resource, FeedUtils.FZ_FEED,
                 FeedUtils.rdf.GetResource(this.feed.url), true);
 
     let currentValue;
     if (ds.hasArcOut(resource, FeedUtils.FZ_STORED))
     {
       currentValue = ds.GetTarget(resource, FeedUtils.FZ_STORED, true);
-      ds.Change(resource, FeedUtils.FZ_STORED, 
+      ds.Change(resource, FeedUtils.FZ_STORED,
                 currentValue, FeedUtils.RDF_LITERAL_TRUE);
     }
     else
       ds.Assert(resource, FeedUtils.FZ_STORED,
                 FeedUtils.RDF_LITERAL_TRUE, true);
   },
 
   mimeEncodeSubject: function(aSubject, aCharset)
@@ -371,24 +371,24 @@ FeedItem.prototype =
 /**
  * Autotag messages.
  *
  * @param  nsIMsgDBHdr aMsgDBHdr - message to tag
  * @param  array aKeywords       - keywords (tags)
  */
   tagItem: function(aMsgDBHdr, aKeywords)
   {
-    let categoryPrefs = this.feed.categoryPrefs();
-    if (!aKeywords.length || !categoryPrefs.enabled)
+    let category = this.feed.options.category;
+    if (!aKeywords.length || !category.enabled)
       return;
 
     let msgArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     msgArray.appendElement(aMsgDBHdr, false);
 
-    let prefix = categoryPrefs.prefixEnabled ? categoryPrefs.prefix : "";
+    let prefix = category.prefixEnabled ? category.prefix : "";
     let rtl = Services.prefs.getIntPref("bidi.direction") == 2;
 
     let keys = [];
     for (let keyword of aKeywords)
     {
       keyword = rtl ? keyword + prefix : prefix + keyword;
       let keyForTag = MailServices.tags.getKeyForTag(keyword);
       if (!keyForTag)
--- a/mailnews/extensions/newsblog/content/FeedUtils.jsm
+++ b/mailnews/extensions/newsblog/content/FeedUtils.jsm
@@ -36,16 +36,21 @@ var FeedUtils = {
   get RSS_ITEM()        { return this.rdf.GetResource(this.RSS_NS + "item") },
   get RSS_LINK()        { return this.rdf.GetResource(this.RSS_NS + "link") },
 
   RSS_CONTENT_NS: "http://purl.org/rss/1.0/modules/content/",
   get RSS_CONTENT_ENCODED() {
     return this.rdf.GetResource(this.RSS_CONTENT_NS + "encoded");
   },
 
+  RSS_SY_NS: "http://purl.org/rss/1.0/modules/syndication/",
+  RSS_SY_UNITS:      ["hourly", "daily", "weekly", "monthly", "yearly"],
+  kBiffUnitsMinutes: "min",
+  kBiffUnitsDays:    "d",
+
   DC_NS: "http://purl.org/dc/elements/1.1/",
   get DC_CREATOR()      { return this.rdf.GetResource(this.DC_NS + "creator") },
   get DC_SUBJECT()      { return this.rdf.GetResource(this.DC_NS + "subject") },
   get DC_DATE()         { return this.rdf.GetResource(this.DC_NS + "date") },
   get DC_TITLE()        { return this.rdf.GetResource(this.DC_NS + "title") },
   get DC_LASTMODIFIED() { return this.rdf.GetResource(this.DC_NS + "lastModified") },
   get DC_IDENTIFIER()   { return this.rdf.GetResource(this.DC_NS + "identifier") },
 
@@ -84,17 +89,18 @@ var FeedUtils = {
   // Timeout for nonresponse to request, 30 seconds.
   REQUEST_TIMEOUT: 30 * 1000,
 
   // The approximate amount of time, specified in milliseconds, to leave an
   // item in the RDF cache after the item has dissappeared from feeds.
   // The delay is currently one day.
   INVALID_ITEM_PURGE_DELAY: 24 * 60 * 60 * 1000,
 
-  kBiffMinutesDefault: 100,
+  // Polling inverval to check individual feed update interval preference.
+  kBiffPollMinutes: 1,
   kNewsBlogSuccess: 0,
   // Usually means there was an error trying to parse the feed.
   kNewsBlogInvalidFeed: 1,
   // Generic networking failure when trying to download the feed.
   kNewsBlogRequestFailure: 2,
   kNewsBlogFeedIsBusy: 3,
   // For 304 Not Modified; There are no new articles for this feed.
   kNewsBlogNoNewItems: 4,
@@ -144,17 +150,17 @@ var FeedUtils = {
     let serverType = "rss";
     let defaultName = FeedUtils.strings.GetStringFromName("feeds-accountname");
     let i = 2;
     while (MailServices.accounts.findRealServer(userName, hostName, serverType, 0))
       // If "Feeds" exists, try "Feeds-2", then "Feeds-3", etc.
       hostName = hostNamePref + "-" + i++;
 
     server = MailServices.accounts.createIncomingServer(userName, hostName, serverType);
-    server.biffMinutes = FeedUtils.kBiffMinutesDefault;
+    server.biffMinutes = FeedUtils.kBiffPollMinutes;
     server.prettyName = aName ? aName : defaultName;
     server.valid = true;
     let account = MailServices.accounts.createAccount();
     account.incomingServer = server;
 
     // Ensure the Trash folder db (.msf) is created otherwise folder/message
     // deletes will throw until restart creates it.
     server.msgStore.discoverSubFolders(server.rootMsgFolder, false);
@@ -212,29 +218,47 @@ var FeedUtils = {
  * Download a feed url on biff or get new messages.
  *
  * @param   nsIMsgFolder aFolder         - folder
  * @param   nsIUrlListener aUrlListener  - feed url
  * @param   bool aIsBiff                 - true if biff, false if manual get
  * @param   nsIDOMWindow aMsgWindow      - window
  */
   downloadFeed: function(aFolder, aUrlListener, aIsBiff, aMsgWindow) {
+    FeedUtils.log.debug("downloadFeed: account loginAtStartUp:isBiff:isOffline - " +
+                        aFolder.server.loginAtStartUp + " : " +
+                        aIsBiff + " : " + Services.io.offline);
     if (Services.io.offline)
       return;
 
     // We don't yet support the ability to check for new articles while we are
     // in the middle of subscribing to a feed. For now, abort the check for
     // new feeds.
     if (FeedUtils.progressNotifier.mSubscribeMode)
     {
       FeedUtils.log.warn("downloadFeed: Aborting RSS New Mail Check. " +
                          "Feed subscription in progress\n");
       return;
     }
 
+    let forceDownload = !aIsBiff;
+    if (aFolder.isServer)
+    {
+      // The lastUpdateTime is |null| only at startup/initialization. Check
+      // if download at startup is wanted; this overrides individual feed
+      // lastUpdateTime or enabled status, just like manual get messages.
+      // Setting loginAtStartUp is not very useful for feeds, as the biff poll
+      // will go off in about kBiffPollMinutes (1) anyway and process each feed
+      // according to its own lastUpdatTime/update frequency.
+      if (FeedUtils.getStatus(aFolder, aFolder.URI).lastUpdateTime === null)
+        forceDownload = aFolder.server.loginAtStartUp;
+
+      FeedUtils.setStatus(aFolder, aFolder.URI, "lastUpdateTime", Date.now());
+    }
+
     let allFolders = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     if (!aFolder.isServer) {
       // Add the base folder; it does not get returned by ListDescendants. Do not
       // add the account folder as it doesn't have the feedUrl property or even
       // a msgDatabase necessarily.
       allFolders.appendElement(aFolder, false);
     }
 
@@ -242,44 +266,71 @@ var FeedUtils = {
 
     let folder;
     function* feeder() {
       let numFolders = allFolders.length;
       for (let i = 0; i < numFolders; i++) {
         folder = allFolders.queryElementAt(i, Ci.nsIMsgFolder);
         FeedUtils.log.debug("downloadFeed: START x/# foldername:uri - " +
                             (i+1) + "/" + numFolders + " " +
-                            folder.name + ":" + folder.URI);
-
-        // Ensure folder's msgDatabase is openable for new message processing.
-        // If not, reparse. After the async reparse the folder will be ready
-        // for the next cycle; don't bother with a listener. Continue with
-        // the next folder, as attempting to add a message to a folder with
-        // an unavailable msgDatabase will throw later.
-        if (!FeedUtils.isMsgDatabaseOpenable(folder, true))
-          continue;
+                            folder.name + " : " + folder.URI);
 
         let feedUrlArray = FeedUtils.getFeedUrlsInFolder(folder);
         // Continue if there are no feedUrls for the folder in the feeds
         // database.  All folders in Trash are skipped.
         if (!feedUrlArray)
           continue;
 
         FeedUtils.log.debug("downloadFeed: CONTINUE foldername:urlArray - " +
-                            folder.name + ":" + feedUrlArray);
+                            folder.name + " : " + feedUrlArray);
 
         FeedUtils.progressNotifier.init(aMsgWindow, false);
 
         // We need to kick off a download for each feed.
+        let now = Date.now();
+        let msgDbOk = false;
         let id, feed;
         for (let url of feedUrlArray)
         {
+          // Ensure folder's msgDatabase is openable for new message processing.
+          // If not, reparse and break (to the next folder), as attempting to
+          // add a message to a folder with an unavailable msgDatabase will
+          // throw later. After the async reparse the folder will be ready for
+          // the next poll cycle to pick up this folder's feeds, whose update
+          // time will not have changed.
+          if (!msgDbOk) {
+            // Only do this test once on the first url.
+            msgDbOk = FeedUtils.isMsgDatabaseOpenable(folder, true);
+            if (!msgDbOk)
+              break;
+          }
+
+          // Check whether this feed should be updated; if forceDownload is true
+          // skip the per feed check.
+          if (!forceDownload)
+          {
+            let status = FeedUtils.getStatus(folder, url);
+            if (!status.enabled ||
+                (now - status.lastUpdateTime < status.updateMinutes * 60000))
+            {
+              FeedUtils.log.debug("downloadFeed: SKIP feed, " +
+                                  "aIsBiff:enabled:minsSinceLastUpdate::url - " +
+                                  aIsBiff + " : " + status.enabled + " : " +
+                                  (Math.round((now - status.lastUpdateTime) / 60) / 1000) +" :: "+
+                                  url);
+              continue;
+            }
+          }
+
+          // Update this feed.
           id = FeedUtils.rdf.GetResource(url);
           feed = new Feed(id, folder.server);
           feed.folder = folder;
+          // Set status info.
+          FeedUtils.setStatus(folder, url, "code", FeedUtils.kNewsBlogFeedIsBusy);
           // Bump our pending feed download count.
           FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
           feed.download(true, FeedUtils.progressNotifier);
           FeedUtils.log.debug("downloadFeed: DOWNLOAD feed url - " + url);
 
           Services.tm.mainThread.dispatch(function() {
             try {
               let done = getFeed.next().done;
@@ -380,30 +431,94 @@ var FeedUtils = {
     {
       aMsgWindow.statusFeedback.showStatusString(
         FeedUtils.strings.GetStringFromName("subscribe-feedAlreadySubscribed"));
       return;
     }
 
     let itemResource = FeedUtils.rdf.GetResource(aUrl);
     let feed = new Feed(itemResource, aFolder.server);
+    // Default setting for new feeds per account settings.
     feed.quickMode = feed.server.getBoolValue("quickMode");
     feed.options = FeedUtils.getOptionsAcct(feed.server);
 
     // If the root server, create a new folder for the feed.  The user must
     // want us to add this subscription url to an existing RSS folder.
     if (!aFolder.isServer)
       feed.folder = aFolder;
 
     FeedUtils.progressNotifier.init(aMsgWindow, true);
     FeedUtils.progressNotifier.mNumPendingFeedDownloads++;
     feed.download(true, FeedUtils.progressNotifier);
   },
 
 /**
+ * Enable or disable updates for all subscriptions in a folder, or all
+ * subscriptions in an account if the folder is the account folder.
+ *
+ * @param   nsIMsgFolder aFolder     - folder or account folder (server).
+ * @param   bool aPause              - to pause or not to pause.
+ * @param   bool aBiffNow            - if aPause is false, and aBiffNow is true
+ *                                     do the biff immediately.
+ */
+  pauseFeedFolderUpdates: function(aFolder, aPause, aBiffNow) {
+    if (aFolder.isServer)
+    {
+      let serverFolder = aFolder.server.rootFolder;
+      // Remove server from biff first. If enabling biff, this will make the
+      // latest biffMinutes take effect now rather than waiting for the timer
+      // to expire.
+      aFolder.server.doBiff = false;
+      if (!aPause)
+        aFolder.server.doBiff = true;
+
+      FeedUtils.setStatus(serverFolder, serverFolder.URI, "enabled", !aPause);
+      if (!aPause && aBiffNow)
+        aFolder.server.performBiff(null);
+
+      return;
+    }
+
+    let feedUrls = FeedUtils.getFeedUrlsInFolder(aFolder);
+    if (!feedUrls)
+      return;
+
+    for (let feedUrl of feedUrls)
+    {
+      let resource = FeedUtils.rdf.GetResource(feedUrl);
+      let feed = new Feed(resource, aFolder.server);
+      let options = feed.options;
+      options.updates.enabled = !aPause;
+      feed.options = options;
+      FeedUtils.setStatus(aFolder, feedUrl, "enabled", !aPause);
+FeedUtils.log.debug("pauseFeedFolderUpdates: enabled:url " + !aPause + ": " + feedUrl);
+    }
+
+    let win = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+    if (win)
+    {
+      let curItem = win.FeedSubscriptions.mView.currentItem;
+      win.FeedSubscriptions.refreshSubscriptionView();
+      if (curItem.container)
+        win.FeedSubscriptions.selectFolder(curItem.folder);
+      else
+      {
+        let resource = FeedUtils.rdf.GetResource(curItem.url);
+        let feed = new Feed(resource, curItem.parentFolder.server);
+        feed.folder = curItem.parentFolder;
+        win.FeedSubscriptions.selectFeed(feed);
+      }
+    }
+
+    this.getSubscriptionsDS(aFolder.server).Flush();
+
+    feed = null;
+  },
+
+/**
  * Add a feed record to the feeds.rdf database and update the folder's feedUrl
  * property.
  *
  * @param  object aFeed - our feed object
  */
   addFeed: function(aFeed) {
     let ds = this.getSubscriptionsDS(aFeed.folder.server);
     let feeds = this.getSubscriptionsList(ds);
@@ -459,16 +574,18 @@ var FeedUtils = {
     // Remove all assertions about items in the feed from the items database.
     let itemds = this.getItemsDS(aServer);
     feed.invalidateItems();
     feed.removeInvalidItems(true);
     itemds.Flush();
 
     // Update folderpane.
     this.setFolderPaneProperty(aParentFolder, "favicon", null, "row");
+    // Remove from cache.
+    delete this[aParentFolder.server.serverURI][feed.url];
 
     feed = null;
   },
 
 /**
  * Change an existing feed's url, as identified by FZ_FEED resource in the
  * feeds.rdf subscriptions database.
  *
@@ -512,17 +629,17 @@ var FeedUtils = {
 /**
  * Get the list of feed urls for a folder, as identified by the FZ_DESTFOLDER
  * tag, directly from the primary feeds.rdf subscriptions database.
  *
  * @param  nsIMsgFolder - the folder.
  * @return array of urls, or null if none.
  */
   getFeedUrlsInFolder: function(aFolder) {
-    if (aFolder.isServer || aFolder.server.type != "rss" ||
+    if (!aFolder || aFolder.isServer || aFolder.server.type != "rss" ||
         aFolder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
         aFolder.getFlag(Ci.nsMsgFolderFlags.Virtual) ||
         !aFolder.filePath.exists())
       // There are never any feedUrls in the account/non-feed/trash/virtual
       // folders or in a ghost folder (nonexistant on disk yet found in
       // aFolder.subFolders).
       return null;
 
@@ -578,41 +695,161 @@ var FeedUtils = {
       aFolder.QueryInterface(Ci.nsIMsgLocalMailFolder)
              .getDatabaseWithReparse(null, null);
     }
     catch (ex) {}
 
     return false;
   },
 
+  /**
+   * Return properties for nsITreeView getCellProperties, for a tree row item in
+   * folderpane or subscribe dialog tree.
+   *
+   * @param   nsIMsgFolder aFolder - folder or a feed url's parent folder
+   * @param   string aFeedUrl      - feed url for a feed row, null for folder
+   * @return  string               - properties
+   */
+  getFolderProperties: function(aFolder, aFeedUrl) {
+    let folder = aFolder;
+    let feedUrls = aFeedUrl ? [aFeedUrl] : this.getFeedUrlsInFolder(aFolder);
+    if (!feedUrls && !folder.isServer)
+      return "";
+
+    let serverEnabled = this.getStatus(folder.server.rootFolder,
+                                       folder.server.rootFolder.URI).enabled;
+    if (folder.isServer)
+      return !serverEnabled ? " serverIsPaused" : "";
+
+    let properties = aFeedUrl ? " isFeed-true" : " isFeedFolder-true";
+    let hasError, isBusy, numPaused = 0;
+    for (let feedUrl of feedUrls) {
+      let feedStatus = this.getStatus(folder, feedUrl);
+      if (feedStatus.code == FeedUtils.kNewsBlogInvalidFeed ||
+          feedStatus.code == FeedUtils.kNewsBlogRequestFailure ||
+          feedStatus.code == FeedUtils.kNewsBlogBadCertError ||
+          feedStatus.code == FeedUtils.kNewsBlogNoAuthError)
+        hasError = true;
+      if (feedStatus.code == FeedUtils.kNewsBlogFeedIsBusy)
+        isBusy = true;
+      if (!feedStatus.enabled)
+        numPaused++;
+    }
+
+    properties += hasError ? " hasError" : "";
+    properties += isBusy ? " isBusy" : "";
+    properties += numPaused == feedUrls.length ? " isPaused" : "";
+    properties += !serverEnabled ? " serverIsPaused" : "";
+
+    return properties;
+  },
+
 /**
  * Update a folderpane cached property.
  *
  * @param  nsIMsgFolder aFolder   - folder
  * @param  string aProperty       - property
  * @param  string aValue          - value
  * @param  string aInvalidate     - "row" = folder's row.
  *                                  "all" = all rows.
  */
   setFolderPaneProperty: function(aFolder, aProperty, aValue, aInvalidate) {
     let win = Services.wm.getMostRecentWindow("mail:3pane");
     if (!aFolder || !aProperty || !win || !("gFolderTreeView" in win))
       return;
 
     win.gFolderTreeView.setFolderCacheProperty(aFolder, aProperty, aValue);
 
-    if (aInvalidate == "all") {
+    if (aInvalidate == "all")
       win.gFolderTreeView._tree.invalidate();
-    }
+
     if (aInvalidate == "row") {
       let row = win.gFolderTreeView.getIndexOfFolder(aFolder);
       win.gFolderTreeView._tree.invalidateRow(row);
     }
   },
 
+  /**
+   * Get a cached feed or folder status.
+   *
+   * @param  nsIMsgFolder aFolder   - folder
+   * @param  string aUrl            - url key (feed url or folder URI)
+   * @param  string aProperty       - url status property
+   * @return string aValue          - value
+   */
+  getStatus: function(aFolder, aUrl) {
+    if (!aFolder || !aUrl)
+      return;
+
+    let serverKey = aFolder.server.serverURI;
+    if (!this[serverKey])
+      this[serverKey] = {};
+
+    if (!this[serverKey][aUrl]) {
+      // Seed the status object.
+      this[serverKey][aUrl] = {};
+      this[serverKey][aUrl]["status"] = this.statusTemplate;
+      if (FeedUtils.isValidScheme(aUrl)) {
+        // Seed persisted status properties for feed urls.
+        let feedResource = FeedUtils.rdf.GetResource(aUrl);
+        let feed = new Feed(feedResource, aFolder.server);
+        this[serverKey][aUrl]["status"].enabled =
+          feed.options.updates.enabled;
+        this[serverKey][aUrl]["status"].updateMinutes =
+          feed.options.updates.updateMinutes;
+        this[serverKey][aUrl]["status"].lastUpdateTime =
+          feed.options.updates.lastUpdateTime;
+        feed = null;
+      }
+      else {
+        // Seed persisted status properties for servers.
+        let optionsAcct = FeedUtils.getOptionsAcct(aFolder.server);
+        this[serverKey][aUrl]["status"].enabled = optionsAcct.doBiff;
+      }
+      FeedUtils.log.debug("getStatus: seed url - " + aUrl);
+    }
+
+    return this[serverKey][aUrl].status;
+  },
+
+  /**
+   * Update a feed or folder status and refresh folderpane.
+   *
+   * @param  nsIMsgFolder aFolder   - folder
+   * @param  string aUrl            - url key (feed url or folder URI)
+   * @param  string aProperty       - url status property
+   * @param  string aValue          - value
+   */
+  setStatus: function(aFolder, aUrl, aProperty, aValue) {
+    if (!aFolder || !aUrl || !aProperty)
+      return;
+
+    if (!this[aFolder.server.serverURI][aUrl])
+      // Not yet seeded, so do it.
+      this.getStatus(aFolder, aUrl);
+
+    this[aFolder.server.serverURI][aUrl]["status"][aProperty] = aValue;
+
+    let win = Services.wm.getMostRecentWindow("mail:3pane");
+    if (win && "gFolderTreeView" in win)
+    {
+      if (aFolder.isServer) {
+        win.gFolderTreeView._tree.invalidate();
+      }
+      else {
+        let row = win.gFolderTreeView.getIndexOfFolder(aFolder);
+        win.gFolderTreeView._tree.invalidateRow(row);
+      }
+    }
+
+    win = Services.wm.getMostRecentWindow("Mail:News-BlogSubscriptions");
+    if (win)
+      win.FeedSubscriptions.mView.treeBox.invalidate();
+  },
+
 /**
  * Get the favicon for a feed folder subscription url (first one) or a feed
  * message url. The favicon service caches it in memory if places history is
  * not enabled.
  *
  * @param  nsIMsgFolder aFolder - the feed folder or null if aUrl
  * @param  string aUrl          - a url (feed, message, other) or null if aFolder
  * @param  string aIconUrl      - the icon url if already determined, else null
@@ -899,57 +1136,177 @@ var FeedUtils = {
     {
       folderName = folderNameBase + "-" + i++;
     }
 
     return folderName;
   },
 
 /**
- * This object will contain all feed specific properties.
+ * This object contains feed/account status info.
+ */
+  _statusDefault: {
+    // Derived from persisted value.
+    enabled: null,
+    // Last update result code, a kNewsBlog* value.
+    code: 0,
+    updateMinutes: null,
+    // JS Date; startup state is null indicating no update since startup.
+    lastUpdateTime: null
+  },
+
+  get statusTemplate()
+  {
+    // Copy the object.
+    return JSON.parse(JSON.stringify(this._statusDefault));
+  },
+
+/**
+ * This object will contain all persisted feed specific properties.
  */
   _optionsDefault: {
-    version: 1,
+    version: 2,
+    updates: {
+      enabled: true,
+      // User set.
+      updateMinutes: 100,
+      // User set: "min"=minutes, "d"=days
+      updateUnits: "min",
+      // JS Date.
+      lastUpdateTime: null,
+      // The last time a new message was stored. JS Date.
+      lastDownloadTime: null,
+      // Publisher recommended from the feed.
+      updatePeriod: null,
+      updateFrequency: 1,
+      updateBase: null
+    },
     // Autotag and <category> handling options.
     category: {
       enabled: false,
       prefixEnabled: false,
       prefix: null,
     }
   },
 
   get optionsTemplate()
   {
     // Copy the object.
     return JSON.parse(JSON.stringify(this._optionsDefault));
   },
 
   getOptionsAcct: function(aServer)
   {
+    let optionsAcct;
     let optionsAcctPref = "mail.server." + aServer.key + ".feed_options";
+    let check_new_mail = "mail.server." + aServer.key + ".check_new_mail";
+    let check_time = "mail.server." + aServer.key + ".check_time";
+
+    // Biff enabled or not. Make sure pref exists.
+    if (!Services.prefs.prefHasUserValue(check_new_mail))
+      Services.prefs.setBoolPref(check_new_mail, true);
+
+    // System polling interval. Make sure pref exists.
+    if (!Services.prefs.prefHasUserValue(check_time))
+      Services.prefs.setIntPref(check_time, FeedUtils.kBiffPollMinutes);
+
     try {
-      return JSON.parse(Services.prefs.getCharPref(optionsAcctPref));
+      optionsAcct = JSON.parse(Services.prefs.getCharPref(optionsAcctPref));
+      // Add the server specific biff enabled state.
+      optionsAcct["doBiff"] = Services.prefs.getBoolPref(check_new_mail);
     }
-    catch (ex) {
-      this.setOptionsAcct(aServer, this._optionsDefault);
-      return JSON.parse(Services.prefs.getCharPref(optionsAcctPref));
-    }
+    catch (ex) {}
+
+    if (optionsAcct && optionsAcct.version == this._optionsDefault.version)
+      return optionsAcct;
+
+    // Init account updates options if new or upgrading to version in
+    // |_optionsDefault.version|.
+    if (!optionsAcct || optionsAcct.version < this._optionsDefault.version)
+      this.initAcct(aServer);
+
+    let newOptions = this.newOptions(optionsAcct);
+    this.setOptionsAcct(aServer, newOptions);
+    newOptions["doBiff"] = Services.prefs.getBoolPref(check_new_mail);
+    return newOptions;
   },
 
   setOptionsAcct: function(aServer, aOptions)
   {
     let optionsAcctPref = "mail.server." + aServer.key + ".feed_options";
-    let newOptions = this.newOptions(aOptions);
-    Services.prefs.setCharPref(optionsAcctPref, JSON.stringify(newOptions));
+    aOptions = aOptions || this.optionsTemplate;
+    Services.prefs.setCharPref(optionsAcctPref, JSON.stringify(aOptions));
+  },
+
+  initAcct: function(aServer)
+  {
+    let serverPrefStr = "mail.server." + aServer.key;
+    // System polling interval. Effective after current interval expires on
+    // change; no user facing ui.
+    Services.prefs.setIntPref(serverPrefStr + ".check_time",
+                              FeedUtils.kBiffPollMinutes);
+
+    // If this pref is false, polling on biff is disabled and account updates
+    // are paused; ui in account server settings and folderpane context menu
+    // (Pause All Updates). Checking Enable updates or unchecking Pause takes
+    // effect immediately. Here on startup, we just ensure the polling interval
+    // above is reset immediately.
+    let doBiff = Services.prefs.getBoolPref(serverPrefStr + ".check_new_mail");
+    FeedUtils.log.debug("initAcct: " + aServer.prettyName + " doBiff - " + doBiff);
+    this.pauseFeedFolderUpdates(aServer.rootFolder, !doBiff, false);
   },
 
-  newOptions: function(aOptions)
+  newOptions: function(aCurrentOptions)
   {
-    // TODO: Clean options, so that only keys in the active template are stored.
-    return aOptions;
+    if (!aCurrentOptions)
+      return this.optionsTemplate;
+
+    // Options version has changed; meld current template with existing
+    // aCurrentOptions settings, removing items gone from the template while
+    // preserving user settings for retained template items.
+    let newOptions = this.optionsTemplate;
+    this.Mixins.meld(aCurrentOptions, false, true).into(newOptions);
+    newOptions.version = this.optionsTemplate.version;
+    return newOptions;
+  },
+
+/**
+ * A generalized recursive melder of two objects. Getters/setters not included.
+ */
+  Mixins: {
+    meld: function(source, keep, replace) {
+      function meldin(source, target, keep, replace) {
+        for (let attribute in source) {
+          // Recurse for objects.
+          if (typeof source[attribute] == "object" &&
+              typeof target[attribute] == "object") {
+            meldin(source[attribute], target[attribute], keep, replace);
+          }
+          else {
+            // Use attribute values from source for the target, unless
+            // replace is false.
+            if (attribute in target && !replace)
+              continue;
+
+            // Don't copy attribute from source to target if it is not in the
+            // target, unless keep is true.
+            if (!(attribute in target) && !keep)
+              continue;
+
+            target[attribute] = source[attribute];
+          }
+        }
+      };
+      return {
+        source: source,
+        into: function(target) {
+          meldin(this.source, target, keep, replace);
+        }
+      };
+    }
   },
 
   getSubscriptionsDS: function(aServer) {
     if (this[aServer.serverURI] && this[aServer.serverURI]["FeedsDS"])
       return this[aServer.serverURI]["FeedsDS"];
 
     let file = this.getSubscriptionsFile(aServer);
     let url = Services.io.getProtocolHandler("file").
@@ -1459,20 +1816,40 @@ var FeedUtils = {
           // Non success.  Remove intermediate traces from the feeds database.
           if (feed && feed.url && feed.server)
             FeedUtils.deleteFeed(FeedUtils.rdf.GetResource(feed.url),
                                  feed.server,
                                  feed.server.rootFolder);
         }
       }
 
-      if (feed.folder && aErrorCode != FeedUtils.kNewsBlogFeedIsBusy)
-        // Free msgDatabase after new mail biff is set; if busy let the next
-        // result do the freeing.  Otherwise new messages won't be indicated.
-        feed.folder.msgDatabase = null;
+      if (aErrorCode != FeedUtils.kNewsBlogFeedIsBusy)
+      {
+        if (aErrorCode == FeedUtils.kNewsBlogSuccess ||
+            aErrorCode == FeedUtils.kNewsBlogNoNewItems)
+        {
+          // Update lastUpdateTime only if successful normal processing.
+          let options = feed.options;
+          let now = Date.now();
+          options.updates.lastUpdateTime = now;
+          if (feed.itemsStored)
+            options.updates.lastDownloadTime = now;
+
+          feed.options = options;
+          FeedUtils.setStatus(feed.folder, feed.url, "lastUpdateTime", now);
+        }
+
+        if (!this.mSubscribeMode)
+          FeedUtils.setStatus(feed.folder, feed.url, "code", aErrorCode);
+
+        if (feed.folder)
+          // Free msgDatabase after new mail biff is set; if busy let the next
+          // result do the freeing.  Otherwise new messages won't be indicated.
+          feed.folder.msgDatabase = null;
+      }
 
       let message = "";
       if (feed.folder)
         location = FeedUtils.getFolderPrettyPath(feed.folder) + " -> ";
       switch (aErrorCode) {
         case FeedUtils.kNewsBlogSuccess:
         case FeedUtils.kNewsBlogFeedIsBusy:
           message = "";
@@ -1594,16 +1971,21 @@ XPCOMUtils.defineLazyGetter(FeedUtils, "
   return Log4Moz.getConfiguredLogger("Feeds");
 });
 
 XPCOMUtils.defineLazyGetter(FeedUtils, "strings", function() {
   return Services.strings.createBundle(
     "chrome://messenger-newsblog/locale/newsblog.properties");
 });
 
+XPCOMUtils.defineLazyGetter(FeedUtils, "stringsPrefs", function() {
+  return Services.strings.createBundle(
+    "chrome://messenger/locale/prefs.properties");
+});
+
 XPCOMUtils.defineLazyGetter(FeedUtils, "rdf", function() {
   return Cc["@mozilla.org/rdf/rdf-service;1"].
          getService(Ci.nsIRDFService);
 });
 
 XPCOMUtils.defineLazyGetter(FeedUtils, "rdfContainerUtils", function() {
   return Cc["@mozilla.org/rdf/container-utils;1"].
          getService(Ci.nsIRDFContainerUtils);
--- a/mailnews/extensions/newsblog/content/am-newsblog.js
+++ b/mailnews/extensions/newsblog/content/am-newsblog.js
@@ -1,63 +1,89 @@
 /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  * 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/. */
 
 Components.utils.import("resource:///modules/FeedUtils.jsm");
 
-var gServer, autotagEnable, autotagUsePrefix, autotagPrefix;
+var gServer, gUpdateEnabled, gUpdateValue, gBiffUnits,
+    gAutotagEnable, gAutotagUsePrefix, gAutotagPrefix;
 
 function onInit(aPageId, aServerId)
 {
   var accountName = document.getElementById("server.prettyName");
   var title = document.getElementById("am-newsblog-title");
   var defaultTitle = title.getAttribute("defaultTitle");
 
   var titleValue;
   if (accountName.value)
     titleValue = defaultTitle + " - <" + accountName.value + ">";
   else
     titleValue = defaultTitle;
 
   title.setAttribute("title", titleValue);
   document.title = titleValue;
 
-  onCheckItem("server.biffMinutes", ["server.doBiff"]);
+  let optionsAcct = FeedUtils.getOptionsAcct(gServer);
+  document.getElementById("doBiff").checked = optionsAcct.doBiff;
 
-  autotagEnable = document.getElementById("autotagEnable");
-  autotagUsePrefix = document.getElementById("autotagUsePrefix");
-  autotagPrefix = document.getElementById("autotagPrefix");
+  gUpdateEnabled = document.getElementById("updateEnabled");
+  gUpdateValue = document.getElementById("updateValue");
+  gBiffUnits = document.getElementById("biffUnits");
+  gAutotagEnable = document.getElementById("autotagEnable");
+  gAutotagUsePrefix = document.getElementById("autotagUsePrefix");
+  gAutotagPrefix = document.getElementById("autotagPrefix");
 
-  let categoryPrefsAcct = FeedUtils.getOptionsAcct(gServer).category;
-  autotagEnable.checked = categoryPrefsAcct.enabled;
-  autotagUsePrefix.disabled = !autotagEnable.checked;
-  autotagUsePrefix.checked = categoryPrefsAcct.prefixEnabled;
-  autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
-  autotagPrefix.value = categoryPrefsAcct.prefix;
+  gUpdateEnabled.checked = optionsAcct.updates.enabled;
+  gBiffUnits.value = optionsAcct.updates.updateUnits;
+  let minutes = optionsAcct.updates.updateUnits == FeedUtils.kBiffUnitsMinutes ?
+                  optionsAcct.updates.updateMinutes :
+                  optionsAcct.updates.updateMinutes / (24 * 60);
+  gUpdateValue.valueNumber = minutes;
+  onCheckItem("updateValue", ["updateEnabled"]);
+
+  gAutotagEnable.checked = optionsAcct.category.enabled;
+  gAutotagUsePrefix.disabled = !gAutotagEnable.checked;
+  gAutotagUsePrefix.checked = optionsAcct.category.prefixEnabled;
+  gAutotagPrefix.disabled = gAutotagUsePrefix.disabled || !gAutotagUsePrefix.checked;
+  gAutotagPrefix.value = optionsAcct.category.prefix;
 }
 
 function onPreInit(account, accountValues)
 {
   gServer = account.incomingServer;
 }
 
-function setCategoryPrefs(aNode)
+function setPrefs(aNode)
 {
-  let options =  FeedUtils.getOptionsAcct(gServer);
+  let optionsAcct =  FeedUtils.getOptionsAcct(gServer);
   switch (aNode.id) {
+    case "doBiff":
+      FeedUtils.pauseFeedFolderUpdates(gServer.rootFolder, !aNode.checked, true);
+      break;
+    case "updateEnabled":
+    case "updateValue":
+    case "biffUnits":
+      optionsAcct.updates.enabled = gUpdateEnabled.checked;
+      onCheckItem("updateValue", ["updateEnabled"]);
+      let minutes = gBiffUnits.value == FeedUtils.kBiffUnitsMinutes ?
+                      gUpdateValue.valueNumber :
+                      gUpdateValue.valueNumber * 24 * 60;
+      optionsAcct.updates.updateMinutes = minutes;
+      optionsAcct.updates.updateUnits = gBiffUnits.value;
+      break;
     case "autotagEnable":
-      options.category.enabled = aNode.checked;
-      autotagUsePrefix.disabled = !aNode.checked;
-      autotagPrefix.disabled = !aNode.checked || !autotagUsePrefix.checked;
+      optionsAcct.category.enabled = aNode.checked;
+      gAutotagUsePrefix.disabled = !aNode.checked;
+      gAutotagPrefix.disabled = !aNode.checked || !gAutotagUsePrefix.checked;
       break;
     case "autotagUsePrefix":
-      options.category.prefixEnabled = aNode.checked;
-      autotagPrefix.disabled = aNode.disabled || !aNode.checked;
+      optionsAcct.category.prefixEnabled = aNode.checked;
+      gAutotagPrefix.disabled = aNode.disabled || !aNode.checked;
       break;
     case "autotagPrefix":
-      options.category.prefix = aNode.value;
+      optionsAcct.category.prefix = aNode.value;
       break;
   }
 
-  FeedUtils.setOptionsAcct(gServer, options)
+  FeedUtils.setOptionsAcct(gServer, optionsAcct)
 }
--- a/mailnews/extensions/newsblog/content/am-newsblog.xul
+++ b/mailnews/extensions/newsblog/content/am-newsblog.xul
@@ -55,67 +55,90 @@
       <caption label="&serverSettings.label;"/>
 
       <checkbox id="server.loginAtStartUp"
                 wsm_persist="true"
                 label="&loginAtStartup.label;"
                 accesskey="&loginAtStartup.accesskey;"
                 prefattribute="value"
                 prefstring="mail.server.%serverkey%.login_at_startup"/>
+      <checkbox id="doBiff"
+                label="&biffAll.label;"
+                accesskey="&biffAll.accesskey;"
+                oncommand="setPrefs(this)"/>
+    </groupbox>
+
+    <separator class="thin"/>
+
+    <groupbox>
+      <caption label="&newFeedSettings.label;"/>
 
       <hbox align="center">
-        <checkbox id="server.doBiff"
-                  wsm_persist="true"
+        <checkbox id="updateEnabled"
                   label="&biffStart.label;"
                   accesskey="&biffStart.accesskey;"
-                  oncommand="onCheckItem('server.biffMinutes', [this.id]);"
-                  prefattribute="value"
-                  prefstring="mail.server.%serverkey%.check_new_mail"/>
-        <textbox id="server.biffMinutes"
-                 wsm_persist="true"
+                  oncommand="setPrefs(this)"/>
+        <textbox id="updateValue"
                  type="number"
                  size="3"
                  min="1"
                  increment="1"
-                 preftype="int"
-                 prefstring="mail.server.%serverkey%.check_time"
-                 aria-labelledby="server.doBiff server.biffMinutes biffEnd"/>
-        <label id="biffEnd"
-             value="&biffEnd.label;"
-             control="server.biffMinutes"/>
+                 aria-labelledby="updateEnabled updateValue biffMinutes biffDays"
+                 onchange="setPrefs(this)"/>
+        <radiogroup id="biffUnits"
+                    orient="horizontal"
+                    oncommand="setPrefs(this)">
+          <radio id="biffMinutes" value="min" label="&biffMinutes.label;"
+                 accesskey="&biffMinutes.accesskey;">
+            <observes element="updateValue" attribute="disabled"/>
+          </radio>
+          <radio id="biffDays" value="d" label="&biffDays.label;"
+                 accesskey="&biffDays.accesskey;">
+            <observes element="updateValue" attribute="disabled"/>
+          </radio>
+        </radiogroup>
       </hbox>
 
       <checkbox id="server.quickMode"
                 wsm_persist="true"
                 genericattr="true"
-                label="&useQuickMode.label;"
-                accesskey="&useQuickMode.accesskey;"
+                label="&quickMode.label;"
+                accesskey="&quickMode.accesskey;"
                 preftype="bool"
                 prefattribute="value"
                 prefstring="mail.server.%serverkey%.quickMode"/>
 
       <checkbox id="autotagEnable"
                 accesskey="&autotagEnable.accesskey;"
                 label="&autotagEnable.label;"
-                oncommand="setCategoryPrefs(this)"/>
+                oncommand="setPrefs(this)"/>
       <hbox>
           <checkbox id="autotagUsePrefix"
                     class="indent"
                     accesskey="&autotagUsePrefix.accesskey;"
                     label="&autotagUsePrefix.label;"
-                    oncommand="setCategoryPrefs(this)"/>
+                    oncommand="setPrefs(this)"/>
           <textbox id="autotagPrefix"
                    placeholder="&autoTagPrefix.placeholder;"
                    clickSelectsAll="true"
-                   onchange="setCategoryPrefs(this)"/>
+                   onchange="setPrefs(this)"/>
       </hbox>
     </groupbox>
 
     <separator class="thin"/>
 
+    <hbox align="center">
+      <button label="&manageSubscriptions.label;"
+              accesskey="&manageSubscriptions.accesskey;"
+              oncommand="openSubscriptionsDialog(gServer.rootFolder);"/>
+      <spacer flex="1"/>
+    </hbox>
+
+    <separator class="thin"/>
+
     <groupbox>
       <caption label="&messageStorage.label;"/>
 
       <checkbox id="server.emptyTrashOnExit"
                 wsm_persist="true"
                 label="&emptyTrashOnExit.label;"
                 accesskey="&emptyTrashOnExit.accesskey;"
                 prefattribute="value"
@@ -137,19 +160,10 @@
                   label="&browseFolder.label;"
                   filepickertitle="&localFolderPicker.label;"
                   accesskey="&browseFolder.accesskey;"
                   oncommand="BrowseForLocalFolders();"/>
         </hbox>
       </vbox>
 
     </groupbox>
-
-    <separator class="thin"/>
-
-    <hbox align="center">
-      <spacer flex="1"/>
-      <button label="&manageSubscriptions.label;"
-              accesskey="&manageSubscriptions.accesskey;"
-              oncommand="openSubscriptionsDialog(gServer.rootFolder);"/>
-    </hbox>
   </vbox>
 </page>
--- a/mailnews/extensions/newsblog/content/feed-parser.js
+++ b/mailnews/extensions/newsblog/content/feed-parser.js
@@ -118,16 +118,18 @@ FeedParser.prototype =
       FeedUtils.log.error("FeedParser.parseAsRSS2: missing mandatory element " +
                           "<title> and <description>, or <link>");
       return aFeed.onParseError(aFeed);
     }
 
     if (!aFeed.parseItems)
       return parsedItems;
 
+    this.findSyUpdateTags(aFeed, channel);
+
     aFeed.invalidateItems();
     // XXX use getElementsByTagNameNS for now; childrenByTagNameNS would be
     // better, but RSS .90 is still with us.
     let itemNodes = aDOM.getElementsByTagNameNS(nsURI, "item");
     itemNodes = itemNodes ? itemNodes : [];
     FeedUtils.log.debug("FeedParser.parseAsRSS2: items to parse - " +
                         itemNodes.length);
 
@@ -344,16 +346,18 @@ FeedParser.prototype =
       FeedUtils.log.error("FeedParser.parseAsRSS1: missing mandatory element " +
                           "<title> and <description>, or <link>");
       return aFeed.onParseError(aFeed);
     }
 
     if (!aFeed.parseItems)
       return parsedItems;
 
+    this.findSyUpdateTags(aFeed, channel, ds);
+
     aFeed.invalidateItems();
 
     // Ignore the <items> list and just get the <item>s.
     let items = ds.GetSources(FeedUtils.RDF_TYPE, FeedUtils.RSS_ITEM, true);
 
     let index = 0;
     while (items.hasMoreElements())
     {
@@ -423,16 +427,18 @@ FeedParser.prototype =
       FeedUtils.log.error("FeedParser.parseAsAtom: missing mandatory element " +
                           "<title>");
       return aFeed.onParseError(aFeed);
     }
 
     if (!aFeed.parseItems)
       return parsedItems;
 
+    this.findSyUpdateTags(aFeed, channel);
+
     aFeed.invalidateItems();
     let items = this.childrenByTagNameNS(channel, FeedUtils.ATOM_03_NS, "entry");
     items = items ? items : [];
     FeedUtils.log.debug("FeedParser.parseAsAtom: items to parse - " +
                         items.length);
 
     for (let itemNode of items)
     {
@@ -508,17 +514,17 @@ FeedParser.prototype =
         for (let j = 0; j < contentNode.childNodes.length; j++)
         {
           let node = contentNode.childNodes.item(j);
           if (node.nodeType == node.CDATA_SECTION_NODE)
             content += node.data;
           else
             content += this.mSerializer.serializeToString(node);
         }
-      
+
         if (contentNode.getAttribute("mode") == "escaped")
         {
           content = content.replace(/&lt;/g, "<");
           content = content.replace(/&gt;/g, ">");
           content = content.replace(/&amp;/g, "&");
         }
 
         if (content == "")
@@ -559,16 +565,18 @@ FeedParser.prototype =
       FeedUtils.log.error("FeedParser.parseAsAtomIETF: missing mandatory element " +
                           "<title>");
       return aFeed.onParseError(aFeed);
     }
 
     if (!aFeed.parseItems)
       return parsedItems;
 
+    this.findSyUpdateTags(aFeed, channel);
+
     aFeed.invalidateItems();
     let items = this.childrenByTagNameNS(channel, FeedUtils.ATOM_IETF_NS, "entry");
     items = items ? items : [];
     FeedUtils.log.debug("FeedParser.parseAsAtomIETF: items to parse - " +
                         items.length);
 
     for (let itemNode of items)
     {
@@ -873,16 +881,71 @@ FeedParser.prototype =
         }
         catch (ex) {}
       }
     }
 
     return null;
   },
 
+  /**
+   * Find RSS Syndication extension tags.
+   * http://web.resource.org/rss/1.0/modules/syndication/
+   *
+   * Feed aFeed           - the feed object.
+   * Node aChannel        - dom node for the <channel>.
+   * nsIRDFDataSource aDS - passed in for rdf feeds only.
+   */
+  findSyUpdateTags: function(aFeed, aChannel, aDS)
+  {
+    let tag, updatePeriod, updateFrequency, updateBase;
+    if (aDS)
+    {
+      // For rdf feeds.
+      tag = FeedUtils.rdf.GetResource(FeedUtils.RSS_SY_NS + "updatePeriod");
+      updatePeriod = this.getRDFTargetValue(aDS, aChannel, tag) || "";
+      tag = FeedUtils.rdf.GetResource(FeedUtils.RSS_SY_NS + "updateFrequency");
+      updateFrequency = this.getRDFTargetValue(aDS, aChannel, tag) || "";
+      tag = FeedUtils.rdf.GetResource(FeedUtils.RSS_SY_NS + "updateBase");
+      updateBase = this.getRDFTargetValue(aDS, aChannel, tag) || "";
+    }
+    else
+    {
+      tag = this.childrenByTagNameNS(aChannel, FeedUtils.RSS_SY_NS, "updatePeriod");
+      updatePeriod = this.getNodeValue(tag ? tag[0] : null) || "";
+      tag = this.childrenByTagNameNS(aChannel, FeedUtils.RSS_SY_NS, "updateFrequency");
+      updateFrequency = this.getNodeValue(tag ? tag[0] : null) || "";
+      tag = this.childrenByTagNameNS(aChannel, FeedUtils.RSS_SY_NS, "updateBase");
+      updateBase = this.getNodeValue(tag ? tag[0] : null) || "";
+    }
+    FeedUtils.log.debug("FeedParser.findSyUpdateTags: updatePeriod:updateFrequency - " +
+                        updatePeriod + ":" + updateFrequency);
+
+    if (updatePeriod)
+    {
+      if (FeedUtils.RSS_SY_UNITS.includes(updatePeriod.toLowerCase()))
+        updatePeriod = updatePeriod.toLowerCase();
+      else
+        updatePeriod = "daily";
+    }
+
+    updateFrequency = isNaN(updateFrequency) ? 1 : updateFrequency;
+
+    let options = aFeed.options;
+    if (options.updates.updatePeriod == updatePeriod &&
+        options.updates.updateFrequency == updateFrequency &&
+        options.updates.updateBase == updateBase)
+      return;
+
+    options.updates.updatePeriod = updatePeriod;
+    options.updates.updateFrequency = updateFrequency;
+    options.updates.updateBase = updateBase;
+    aFeed.options = options;
+  },
+
   stripTags: function(someHTML)
   {
     return someHTML ? someHTML.replace(/<[^>]+>/g, "") : someHTML;
   },
 
   xmlUnescape: function(s)
   {
     s = s.replace(/&lt;/g, "<");
--- a/mailnews/extensions/newsblog/content/feed-subscriptions.js
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.js
@@ -1,16 +1,17 @@
 /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  * 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/. */
 
 Components.utils.import("resource:///modules/FeedUtils.jsm");
 Components.utils.import("resource:///modules/gloda/log4moz.js");
 Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
 Components.utils.import("resource://gre/modules/FileUtils.jsm");
 Components.utils.import("resource://gre/modules/PluralForm.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 var {classes: Cc, interfaces: Ci} = Components;
 
 var FeedSubscriptions = {
   get mMainWin() { return Services.wm.getMostRecentWindow("mail:3pane"); },
@@ -167,48 +168,62 @@ var FeedSubscriptions = {
     getRowProperties: function(aRow)                   { return ""; },
     getColumnProperties: function(aCol)                { return ""; },
     getCellValue: function (aRow, aColumn)             {},
     setCellValue: function (aRow, aColumn, aValue)     {},
     setCellText: function (aRow, aColumn, aValue)      {},
 
     getCellProperties: function (aRow, aColumn) {
       let item = this.getItemAtIndex(aRow);
-      let folder = item && item.folder ? item.folder : null;
-#ifdef MOZ_THUNDERBIRD
-      let properties = ["folderNameCol"];
-      let hasFeeds = folder ? FeedUtils.getFeedUrlsInFolder(folder) : false;
-      let prop = !folder ? "isFeed-true" :
-                 hasFeeds ? "isFeedFolder-true" :
-                 folder.isServer ? "serverType-rss isServer-true" : null;
-      if (prop)
-        properties.push(prop);
-      return properties.join(" ");
-#else
-      return !folder ? "serverType-rss" :
-             folder.isServer ? "serverType-rss isServer-true" : "livemark";
-#endif
+      if (!item)
+        return;
+
+      if (AppConstants.MOZ_APP_NAME != "thunderbird")
+      {
+        return !item.folder ? "serverType-rss" :
+               item.folder.isServer ? "serverType-rss isServer-true" : "livemark";
+      }
+
+      let folder = item.folder;
+      let properties = "folderNameCol";
+      let mainWin = FeedSubscriptions.mMainWin;
+      if (!mainWin)
+      {
+        let hasFeeds = FeedUtils.getFeedUrlsInFolder(folder);
+        properties += !folder ? " isFeed-true" :
+                      hasFeeds ? " isFeedFolder-true" :
+                      folder.isServer ? " serverType-rss isServer-true" : "";
+      }
+      else
+      {
+        let url = folder ? null : item.url;
+        folder = folder || item.parentFolder;
+        properties += mainWin.FeedUtils.getFolderProperties(folder, url);
+      }
+
+      item["properties"] = properties;
+      return properties;
     },
 
     isContainer: function (aRow)
     {
       let item = this.getItemAtIndex(aRow);
       return item ? item.container : false;
     },
 
     isContainerOpen: function (aRow)
-    { 
+    {
       let item = this.getItemAtIndex(aRow);
       return item ? item.open : false;
     },
 
     isContainerEmpty: function (aRow)
-    { 
+    {
       let item = this.getItemAtIndex(aRow);
-      if (!item) 
+      if (!item)
         return false;
 
       return item.children.length == 0;
     },
 
     getItemAtIndex: function (aRow)
     {
       if (aRow < 0 || aRow >= FeedSubscriptions.mFeedContainers.length)
@@ -287,16 +302,21 @@ var FeedSubscriptions = {
     },
 
     getImageSrc: function(aRow, aCol)
     {
       let item = this.getItemAtIndex(aRow);
       if ((item.folder && item.folder.isServer) || item.open)
         return "";
 
+      if (!item.open &&
+          (item.properties.includes("hasError") ||
+           item.properties.includes("isBusy")))
+        return "";
+
       if (item.favicon != null)
         return item.favicon;
 
       if (item.folder && FeedSubscriptions.mMainWin &&
           "gFolderTreeView" in FeedSubscriptions.mMainWin) {
         let favicon = FeedSubscriptions.mMainWin.gFolderTreeView
                                        .getFolderCacheProperty(item.folder, "favicon");
         if (favicon != null)
@@ -344,17 +364,17 @@ var FeedSubscriptions = {
         }, 0);
       }
 
       // Store empty string to return default while favicons are retrieved.
       return item.favicon = "";
     },
 
     canDrop: function (aRow, aOrientation)
-    { 
+    {
       let dropResult = this.extractDragData(aRow);
       return aOrientation == Ci.nsITreeView.DROP_ON && dropResult.canDrop &&
              (dropResult.dropUrl || dropResult.dropOnIndex != this.kRowIndexUndefined);
     },
 
     drop: function (aRow, aOrientation)
     {
       let win = FeedSubscriptions;
@@ -688,31 +708,31 @@ var FeedSubscriptions = {
     FeedSubscriptions.mTree.focus();
   },
 
   /**
    * Find the folder in the tree.  The search may be limited to subfolders of
    * a known folder, or expanded to include the entire tree. This function is
    * also used to insert/remove folders without rebuilding the tree view cache
    * (to avoid position/toggle state loss).
-   * 
+   *
    * @param  aFolder nsIMsgFolder - the folder to find.
    * @param  [aParams] object     - params object, containing:
-   * 
+   *
    * [parentIndex] int        - index of folder to start the search; if
    *                            null (default), the index of the folder's
    *                            rootFolder will be used.
    * [select] boolean         - if true (default) the folder's ancestors
    *                            will be opened and the folder selected.
    * [open] boolean           - if true (default) the folder is opened.
    * [remove] boolean         - delete the item from tree row cache if true,
    *                            false (default) otherwise.
    * [newFolder] nsIMsgFolder - if not null (default) the new folder,
    *                            for add or rename.
-   * 
+   *
    * @return bool found - true if found, false if not.
    */
   selectFolder: function(aFolder, aParms)
   {
     let folderURI = aFolder.URI;
     let parentIndex = aParms && ("parentIndex" in aParms) ? aParms.parentIndex : null;
     let selectIt = aParms && ("select" in aParms) ? aParms.select : true;
     let openIt = aParms && ("open" in aParms) ? aParms.open : true;
@@ -866,20 +886,20 @@ var FeedSubscriptions = {
                         curFirstVisRow + ":" + curLastVisRow + ":" + this.mView.rowCount);
 
     return found;
   },
 
   /**
    * Find the feed in the tree.  The search first gets the feed's folder,
    * then selects the child feed.
-   * 
+   *
    * @param  aFeed {Feed object}    - the feed to find.
    * @param  [aParentIndex] integer - index to start the folder search.
-   * 
+   *
    * @return found bool - true if found, false if not.
    */
   selectFeed: function(aFeed, aParentIndex)
   {
     let folder = aFeed.folder;
     let found = false;
 
     if (aFeed.folder.isServer) {
@@ -949,31 +969,53 @@ var FeedSubscriptions = {
 
     // Common to both folder and feed items.
     nameValue.disabled = aItem.container;
     this.setFolderPicker(displayFolder, isFeed);
 
     // Set quick mode value.
     document.getElementById("quickMode").checked = aItem.quickMode;
 
+    if (isServer)
+      aItem.options = FeedUtils.getOptionsAcct(server);
+
+    // Update items.
+    let updateEnabled = document.getElementById("updateEnabled");
+    let updateValue = document.getElementById("updateValue");
+    let biffUnits = document.getElementById("biffUnits");
+    let recommendedUnits = document.getElementById("recommendedUnits");
+    let recommendedUnitsVal = document.getElementById("recommendedUnitsVal");
+    let updates = aItem.options ? aItem.options.updates :
+                                  FeedUtils._optionsDefault.updates;
+
+    updateEnabled.checked = updates.enabled;
+    updateValue.disabled = !updateEnabled.checked;
+    biffUnits.value = updates.updateUnits;
+    let minutes = updates.updateUnits == FeedUtils.kBiffUnitsMinutes ?
+                    updates.updateMinutes :
+                    updates.updateMinutes / (24 * 60);
+    updateValue.valueNumber = minutes;
+    if (isFeed)
+      recommendedUnitsVal.value = this.getUpdateMinutesRec(updates);
+    else
+      recommendedUnitsVal.value = "";
+
+    recommendedUnits.hidden = recommendedUnitsVal.value == "";
+
     // Autotag items.
     let autotagEnable = document.getElementById("autotagEnable");
     let autotagUsePrefix = document.getElementById("autotagUsePrefix");
     let autotagPrefix = document.getElementById("autotagPrefix");
-    let categoryPrefsAcct = FeedUtils.getOptionsAcct(server).category;
-    if (isServer)
-      aItem.options = FeedUtils.getOptionsAcct(server);
-    let categoryPrefs = aItem.options ? aItem.options.category : null;
-
-    autotagEnable.checked = categoryPrefs && categoryPrefs.enabled;
-    autotagUsePrefix.checked = categoryPrefs && categoryPrefs.prefixEnabled;
+    let category = aItem.options ? aItem.options.category : null;
+
+    autotagEnable.checked = category && category.enabled;
+    autotagUsePrefix.checked = category && category.prefixEnabled;
     autotagUsePrefix.disabled = !autotagEnable.checked;
     autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
-    autotagPrefix.value = categoryPrefs && categoryPrefs.prefix ?
-                            categoryPrefs.prefix : "";
+    autotagPrefix.value = category && category.prefix ? category.prefix : "";
   },
 
   setFolderPicker: function(aFolder, aIsFeed)
   {
     let editFeed = document.getElementById("editFeed");
     let folderPrettyPath = FeedUtils.getFolderPrettyPath(aFolder);
     if (!folderPrettyPath)
       return editFeed.disabled = true;
@@ -1067,37 +1109,50 @@ var FeedSubscriptions = {
     }
 
     // Update the folder in the tree map.
     item.quickMode = aChecked;
     let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
     this.updateStatusItem("statusText", message);
   },
 
-  setCategoryPrefs: function(aNode)
+  setPrefs: function(aNode)
   {
     let item = this.mView.currentItem;
     if (!item)
       return;
 
     let isServer = item.folder && item.folder.isServer;
     let isFolder = item.folder && !item.folder.isServer;
+    let updateEnabled = document.getElementById("updateEnabled");
+    let updateValue = document.getElementById("updateValue");
+    let biffUnits = document.getElementById("biffUnits");
     let autotagEnable = document.getElementById("autotagEnable");
     let autotagUsePrefix = document.getElementById("autotagUsePrefix");
     let autotagPrefix = document.getElementById("autotagPrefix");
     if (isFolder || (isServer && document.getElementById("locationValue").value))
     {
       // Intend to subscribe a feed to a folder, a value must be in the url
       // field. Update states for addFeed() and return.
+      updateValue.disabled = !updateEnabled.checked;
       autotagUsePrefix.disabled = !autotagEnable.checked;
       autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
       return;
     }
 
     switch (aNode.id) {
+      case "updateEnabled":
+      case "biffUnits":
+        item.options.updates.enabled = updateEnabled.checked;
+        let minutes = biffUnits.value == FeedUtils.kBiffUnitsMinutes ?
+                        updateValue.valueNumber :
+                        updateValue.valueNumber * 24 * 60;
+        item.options.updates.updateMinutes = minutes;
+        item.options.updates.updateUnits = biffUnits.value;
+        break;
       case "autotagEnable":
         item.options.category.enabled = aNode.checked;
         break;
       case "autotagUsePrefix":
         item.options.category.prefixEnabled = aNode.checked;
         item.options.category.prefix = autotagPrefix.value;
         break;
     }
@@ -1108,37 +1163,75 @@ var FeedSubscriptions = {
     }
     else
     {
       let feedResource = FeedUtils.rdf.GetResource(item.url);
       let feed = new Feed(feedResource, item.parentFolder.server);
       feed.options = item.options;
       let ds = FeedUtils.getSubscriptionsDS(item.parentFolder.server);
       ds.Flush();
+
+      if (aNode.id == "updateEnabled")
+      {
+        FeedUtils.setStatus(item.parentFolder, item.url, "enabled", aNode.checked);
+        this.mView.selection.tree.invalidateRow(this.mView.selection.currentIndex);
+      }
     }
 
     this.updateFeedData(item);
     let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
     this.updateStatusItem("statusText", message);
   },
 
+  getUpdateMinutesRec: function(aUpdates)
+  {
+    // Assume the parser has stored correct/valid values for the spec. If the
+    // feed doesn't use any of these tags, updatePeriod will be null.
+    if (aUpdates.updatePeriod == null)
+      return "";
+
+    let biffUnits = document.getElementById("biffUnits").value;
+    let units = biffUnits == FeedUtils.kBiffUnitsDays ? 1 : 24 * 60;
+    let frequency = aUpdates.updateFrequency;
+    let val;
+    switch (aUpdates.updatePeriod) {
+      case "hourly":
+        val = biffUnits == FeedUtils.kBiffUnitsDays ? frequency / 24 : 60 / frequency;
+        break;
+      case "daily":
+        val = units / frequency;
+        break;
+      case "weekly":
+        val = 7 * units / frequency;
+        break;
+      case "monthly":
+        val = 30 * units / frequency;
+        break;
+      case "yearly":
+        val = 365 * units / frequency;
+        break;
+    }
+
+    return val ? Math.round(val * 100) / 100 : "";
+  },
+
   onKeyPress: function(aEvent)
   {
     if (aEvent.keyCode == aEvent.DOM_VK_DELETE &&
         aEvent.target.id == "rssSubscriptionsList")
       this.removeFeed(true);
 
     this.clearStatusInfo();
   },
 
   onSelect: function ()
   {
     let item = this.mView.currentItem;
     this.updateFeedData(item);
-    this.setSummaryFocus();
+    this.setFocus();
     this.updateButtons(item);
   },
 
   updateButtons: function (aSelectedItem)
   {
     let item = aSelectedItem;
     let isServer = item && item.folder && item.folder.isServer;
     let disable = !item || !item.container || isServer ||
@@ -1161,33 +1254,36 @@ var FeedSubscriptions = {
     if (aEvent.button != 0 ||
         aEvent.target.id == "validationText" ||
         aEvent.target.id == "addCertException")
       return;
 
     this.clearStatusInfo();
   },
 
-  setSummaryFocus: function ()
+  setFocus: function ()
   {
     let item = this.mView.currentItem;
     if (!item)
       return;
 
     let locationValue = document.getElementById("locationValue");
+    let updateEnabled = document.getElementById("updateEnabled");
+    let updateValue = document.getElementById("updateValue");
     let quickMode = document.getElementById("quickMode");
     let autotagEnable = document.getElementById("autotagEnable");
     let autotagUsePrefix = document.getElementById("autotagUsePrefix");
     let autotagPrefix = document.getElementById("autotagPrefix");
     let isServer = item.folder && item.folder.isServer;
     let isFolder = item.folder && !item.folder.isServer;
     let isFeed = !item.container;
 
-    // Enable summary/autotag by default.
-    quickMode.disabled = autotagEnable.disabled = false;
+    // Enabled by default.
+    updateEnabled.disabled = quickMode.disabled = autotagEnable.disabled = false;
+    updateValue.disabled = !updateEnabled.checked;
     autotagUsePrefix.disabled = !autotagEnable.checked;
     autotagPrefix.disabled = autotagUsePrefix.disabled || !autotagUsePrefix.checked;
 
     if (isServer)
     {
       let disable = locationValue.hasAttribute("focused") || locationValue.value;
       document.getElementById("addFeed").disabled = !disable;
       document.getElementById("editFeed").disabled = disable;
@@ -1195,17 +1291,18 @@ var FeedSubscriptions = {
     }
     else if (isFolder)
     {
       if (!locationValue.hasAttribute("focused") && !locationValue.value)
       {
         // Enabled for a folder with feeds. Autotag disabled unless intent is
         // to add a feed.
         quickMode.disabled = !FeedUtils.getFeedUrlsInFolder(item.folder);
-        autotagEnable.disabled = autotagUsePrefix.disabled =
+        updateEnabled.disabled = updateValue.disabled =
+          autotagEnable.disabled = autotagUsePrefix.disabled =
           autotagPrefix.disabled = true;
       }
     }
     else
     {
       // Summary is per folder.
       quickMode.disabled = true;
     }
@@ -1254,27 +1351,27 @@ var FeedSubscriptions = {
   /**
    * This addFeed is used by 1) Add button, 1) Update button, 3) Drop of a
    * feed url on a folder (which can be an add or move).  If Update, the new
    * url is added and the old removed; thus aParse is false and no new messages
    * are downloaded, the feed is only validated and stored in the db.  If dnd,
    * the drop folder is selected and the url is prefilled, so proceed just as
    * though the url were entered manually.  This allows a user to see the dnd
    * url better in case of errors.
-   * 
+   *
    * @param  [aFeedLocation] string    - the feed url; get the url from the
    *                                     input field if null.
    * @param  [aFolder] nsIMsgFolder    - folder to subscribe, current selected
    *                                     folder if null.
    * @param  [aParse] boolean          - if true (default) parse and download
    *                                     the feed's articles.
    * @param  [aParams] object          - additional params.
    * @param  [aMode] integer           - action mode (default is kSubscribeMode)
    *                                     of the add.
-   * 
+   *
    * @return success boolean           - true if edit checks passed and an
    *                                     async download has been initiated.
    */
   addFeed: function(aFeedLocation, aFolder, aParse, aParams, aMode)
   {
     let message;
     let parse = aParse == null ? true : aParse;
     let mode = aMode == null ? this.kSubscribeMode : aMode;
@@ -1333,16 +1430,22 @@ var FeedSubscriptions = {
       this.updateStatusItem("statusText", message);
       return false;
     }
 
     if (!options)
     {
       // Not passed a param, get values from the ui.
       options = FeedUtils.optionsTemplate;
+      options.updates.enabled = document.getElementById("updateEnabled").checked;
+      let biffUnits = document.getElementById("biffUnits").value;
+      let units = document.getElementById("updateValue").valueNumber;
+      let minutes = biffUnits == FeedUtils.kBiffUnitsMinutes ? units : units * 24 * 60;
+      options.updates.updateUnits = biffUnits;
+      options.updates.updateMinutes = minutes;
       options.category.enabled = document.getElementById("autotagEnable").checked;
       options.category.prefixEnabled = document.getElementById("autotagUsePrefix").checked;
       options.category.prefix = document.getElementById("autotagPrefix").value;
     }
 
     let folderURI = addFolder.isServer ? null : addFolder.URI;
     let feedProperties = { feedName     : name,
                            feedLocation : feedLocation,
@@ -1386,20 +1489,28 @@ var FeedSubscriptions = {
     feed.title = feedProperties.feedName;
     feed.quickMode = feedProperties.quickMode;
     feed.options = feedProperties.options;
     return feed;
   },
 
   updateAccount: function(aItem)
   {
-    // Check to see if the categoryPrefs custom prefix string value changed.
+    // Check to see if a pref value changed.
+    let editUpdateValue = document.getElementById("updateValue").valueNumber;
+    let editBiffUnits = document.getElementById("biffUnits").value;
     let editAutotagPrefix = document.getElementById("autotagPrefix").value;
-    if (aItem.options.category.prefix != editAutotagPrefix)
+    if (aItem.options.updates.updateMinutes != editUpdateValue ||
+        aItem.options.updates.updateUnits != editBiffUnits ||
+        aItem.options.category.prefix != editAutotagPrefix)
     {
+      aItem.options.updates.updateUnits = editBiffUnits;
+      let minutes = editBiffUnits == FeedUtils.kBiffUnitsMinutes ?
+                      editUpdateValue : editUpdateValue * 24 * 60;
+      aItem.options.updates.updateMinutes = minutes;
       aItem.options.category.prefix = editAutotagPrefix;
       FeedUtils.setOptionsAcct(aItem.folder.server, aItem.options)
       let message = FeedUtils.strings.GetStringFromName("subscribe-feedUpdated");
       this.updateStatusItem("statusText", message);
     }
   },
 
   editFeed: function()
@@ -1423,16 +1534,18 @@ var FeedSubscriptions = {
     let ds = FeedUtils.getSubscriptionsDS(currentFolderServer);
     let currentFolderURI = itemToEdit.parentFolder.URI;
     let feed = new Feed(resource, currentFolderServer);
     feed.folder = itemToEdit.parentFolder;
 
     let editNameValue = document.getElementById("nameValue").value;
     let editFeedLocation = document.getElementById("locationValue").value.trim();
     let selectFolder = document.getElementById("selectFolder");
+    let editUpdateValue = document.getElementById("updateValue").valueNumber;
+    let editBiffUnits = document.getElementById("biffUnits").value;
     let editQuickMode = document.getElementById("quickMode").checked;
     let editAutotagPrefix = document.getElementById("autotagPrefix").value;
 
     if (feed.url != editFeedLocation)
     {
       // Updating a url.  We need to add the new url and delete the old, to
       // ensure everything is cleaned up correctly.
       this.addFeed(null, itemToEdit.parentFolder, false, null, this.kUpdateMode)
@@ -1482,25 +1595,39 @@ var FeedSubscriptions = {
       {
         feed.title = editNameValue;
         itemToEdit.name = editNameValue;
         seln.tree.invalidateRow(seln.currentIndex);
         updated = true;
       }
     }
 
+    // Check to see if the updateValue value changed.
+    if (itemToEdit.options.updates.updateMinutes != editUpdateValue ||
+        itemToEdit.options.updates.updateUnits != editBiffUnits)
+    {
+      itemToEdit.options.updates.updateUnits = editBiffUnits;
+      let minutes = editBiffUnits == FeedUtils.kBiffUnitsMinutes ?
+                      editUpdateValue : editUpdateValue * 24 * 60;
+      itemToEdit.options.updates.updateMinutes = minutes;
+      feed.options = itemToEdit.options;
+      FeedUtils.setStatus(itemToEdit.parentFolder, itemToEdit.url,
+                          "updateMinutes", minutes);
+      updated = true;
+    }
+
     // Check to see if the quickMode value changed.
     if (feed.quickMode != editQuickMode)
     {
       feed.quickMode = editQuickMode;
       itemToEdit.quickMode = editQuickMode;
       updated = true;
     }
 
-    // Check to see if the categoryPrefs custom prefix string value changed.
+    // Check to see if the category custom prefix string value changed.
     if (itemToEdit.options.category.prefix != editAutotagPrefix &&
         itemToEdit.options.category.prefix != null &&
         editAutotagPrefix != "")
     {
       itemToEdit.options.category.prefix = editAutotagPrefix;
       feed.options = itemToEdit.options;
       updated = true;
     }
@@ -1521,17 +1648,17 @@ var FeedSubscriptions = {
       this.updateStatusItem("statusText", message);
       this.updateStatusItem("progressMeter", "?");
       feed.download(false, this.mFeedDownloadCallback);
     }, verifyDelay);
   },
 
 /**
  * Moves or copies a feed to another folder or account.
- * 
+ *
  * @param  int aOldFeedIndex    - index in tree of target feed item.
  * @param  int aNewParentIndex  - index in tree of target parent folder item.
  * @param  string aMoveCopy     - either "move" or "copy".
  */
   moveCopyFeed: function(aOldFeedIndex, aNewParentIndex, aMoveCopy)
   {
     let moveFeed = aMoveCopy == "move";
     let currentItem = this.mView.getItemAtIndex(aOldFeedIndex);
@@ -1700,16 +1827,21 @@ var FeedSubscriptions = {
           win.mActionMode = null;
           win.clearStatusInfo();
           message = FeedUtils.strings.GetStringFromName("subscribe-feedVerified");
           win.updateStatusItem("statusText", message);
           document.getElementById("editFeed").removeAttribute("disabled");
           return;
         }
 
+        // Update lastUpdateTime if successful.
+        let options = feed.options;
+        options.updates.lastUpdateTime = Date.now();
+        feed.options = options;
+
         // Add the feed to the databases.
         FeedUtils.addFeed(feed);
 
         // Now add the feed to our view.  If adding, the current selection will
         // be a folder; if updating it will be a feed.  No need to rebuild the
         // entire view, that is too jarring.
         let curIndex = win.mView.selection.currentIndex;
         let curItem = win.mView.getItemAtIndex(curIndex);
@@ -2133,17 +2265,17 @@ var FeedSubscriptions = {
 
   get brandShortName() {
     let brandBundle = document.getElementById("bundle_brand");
     return brandBundle ? brandBundle.getString("brandShortName") : "";
   },
 
 /**
  * Export feeds as opml file Save As filepicker function.
- * 
+ *
  * @param  bool aList - if true, exporting as list; if false (default)
  *                      exporting feeds in folder structure - used for title.
  * @return nsILocalFile or null.
  */
   opmlPickSaveAsFile: function(aList)
   {
     let accountName = this.mRSSServer.rootFolder.prettyName;
     let fileName = FeedUtils.strings.formatStringFromName(
@@ -2173,17 +2305,17 @@ var FeedSubscriptions = {
       return fp.file;
     }
 
     return null;
   },
 
 /**
  * Import feeds opml file Open filepicker function.
- * 
+ *
  * @return nsILocalFile or null.
  */
   opmlPickOpenFile: function()
   {
     let title = FeedUtils.strings.GetStringFromName("subscribe-OPMLImportTitle");
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
 
     fp.defaultString = "";
@@ -2418,21 +2550,21 @@ var FeedSubscriptions = {
       this.updateButtons(item);
       this.clearStatusInfo();
     }
   },
 
 /**
  * Import opml file into a feed account.  Used by the Subscribe dialog and
  * the Import wizard.
- * 
+ *
  * @param  nsILocalFile aFile           - the opml file.
  * @param  nsIMsgIncomingServer aServer - the account server.
  * @param  func aCallback               - callback function.
- * 
+ *
  * @return bool                         - false if error.
  */
   importOPMLFile: function(aFile, aServer, aCallback)
   {
     if (aServer && (aServer instanceof Ci.nsIMsgIncomingServer))
       this.mRSSServer = aServer;
 
     if (!aFile || !this.mRSSServer || !aCallback)
--- a/mailnews/extensions/newsblog/content/feed-subscriptions.xul
+++ b/mailnews/extensions/newsblog/content/feed-subscriptions.xul
@@ -7,16 +7,18 @@
 <?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger-newsblog/skin/feed-subscriptions.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % feedDTD SYSTEM "chrome://messenger-newsblog/locale/feed-subscriptions.dtd">
     %feedDTD;
+  <!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd">
+    %newsblogDTD;
   <!ENTITY % certDTD SYSTEM "chrome://pippki/locale/certManager.dtd">
     %certDTD;
 ]>
 
 <window id="subscriptionsDialog"
         flex="1"
         title="&feedSubscriptions.label;"
         windowtype="Mail:News-BlogSubscriptions"
@@ -101,18 +103,18 @@
                        value="&feedLocation.label;"/>
               </hbox>
               <hbox>
                 <textbox id="locationValue"
                          flex="1"
                          class="uri-element"
                          placeholder="&feedLocation.placeholder;"
                          clickSelectsAll="true"
-                         onfocus="FeedSubscriptions.setSummaryFocus();"
-                         onblur="FeedSubscriptions.setSummaryFocus();"/>
+                         onfocus="FeedSubscriptions.setFocus();"
+                         onblur="FeedSubscriptions.setFocus();"/>
                 <hbox align="center">
                   <label id="locationValidate"
                          collapsed="true"
                          class="text-link"
                          crop="end"
                          value="&locationValidate.label;"
                          onclick="FeedSubscriptions.checkValidation(event);"/>
                 </hbox>
@@ -142,30 +144,70 @@
                          flex="1"
                          readonly="true"
                          onkeypress="FeedSubscriptions.onClickSelectFolderValue(event)"
                          onclick="FeedSubscriptions.onClickSelectFolderValue(event)"/>
               </hbox>
             </row>
           </rows>
         </grid>
+
+        <hbox align="center">
+          <checkbox id="updateEnabled"
+                    label="&biffStart.label;"
+                    accesskey="&biffStart.accesskey;"
+                    oncommand="FeedSubscriptions.setPrefs(this)"/>
+          <textbox id="updateValue"
+                   type="number"
+                   size="3"
+                   min="1"
+                   increment="1"
+                   aria-labelledby="updateEnabled updateValue biffMinutes biffDays recommendedUnits recommendedUnitsVal"/>
+          <radiogroup id="biffUnits"
+                      orient="horizontal"
+                      oncommand="FeedSubscriptions.setPrefs(this)">
+            <radio id="biffMinutes" value="min" label="&biffMinutes.label;"
+                   accesskey="&biffMinutes.accesskey;">
+              <observes element="updateValue" attribute="disabled"/>
+            </radio>
+            <radio id="biffDays" value="d" label="&biffDays.label;"
+                   accesskey="&biffDays.accesskey;">
+              <observes element="updateValue" attribute="disabled"/>
+            </radio>
+          </radiogroup>
+          <hbox id="recommendedBox">
+            <label id="recommendedUnits"
+                   value="&recommendedUnits.label;"
+                   hidden="true"
+                   control="updateMinutes">
+              <observes element="updateValue" attribute="disabled"/>
+            </label>
+            <label id="recommendedUnitsVal"
+                   value=""
+                   hidden="true"
+                   control="updateMinutes">
+              <observes element="updateValue" attribute="disabled"/>
+              <observes element="recommendedUnits" attribute="hidden"/>
+            </label>
+          </hbox>
+        </hbox>
         <checkbox id="quickMode"
                   accesskey="&quickMode.accesskey;"
                   label="&quickMode.label;"
                   oncommand="FeedSubscriptions.setSummary(this.checked)"/>
         <checkbox id="autotagEnable"
                   accesskey="&autotagEnable.accesskey;"
                   label="&autotagEnable.label;"
-                  oncommand="FeedSubscriptions.setCategoryPrefs(this)"/>
+                  oncommand="FeedSubscriptions.setPrefs(this)"/>
         <hbox>
             <checkbox id="autotagUsePrefix"
                       class="indent"
                       accesskey="&autotagUsePrefix.accesskey;"
                       label="&autotagUsePrefix.label;"
-                      oncommand="FeedSubscriptions.setCategoryPrefs(this)"/>
+                      oncommand="FeedSubscriptions.setPrefs(this)"/>
             <textbox id="autotagPrefix"
                      placeholder="&autoTagPrefix.placeholder;"
                      clickSelectsAll="true"/>
         </hbox>
         <separator class="thin"/>
       </vbox>
     </hbox>
 
--- a/mailnews/extensions/newsblog/content/newsblogOverlay.js
+++ b/mailnews/extensions/newsblog/content/newsblogOverlay.js
@@ -16,16 +16,18 @@ var FeedMessageHandler = {
   kSelectOverrideWebPage:   0,
   kSelectOverrideSummary:   1,
   kSelectFeedDefault:       2,
   kOpenWebPage:             0,
   kOpenSummary:             1,
   kOpenToggleInMessagePane: 2,
   kOpenLoadInBrowser:       3,
 
+  FeedAccountTypes: ["rss"],
+
   /**
    * How to load message on threadpane select.
    */
   get onSelectPref() {
     return Services.prefs.getIntPref("rss.show.summary");
   },
 
   set onSelectPref(val) {
@@ -56,19 +58,32 @@ var FeedMessageHandler = {
    * be in an rss acount type folder. In Tb15 and later, a flag is set on the
    * message itself upon initial store; the message can be moved to any folder.
    *
    * @param nsIMsgDBHdr aMsgHdr - the message.
    *
    * @return true if message is a feed, false if not.
    */
   isFeedMessage: function(aMsgHdr) {
-    return (aMsgHdr instanceof Components.interfaces.nsIMsgDBHdr) &&
-           ((aMsgHdr.flags & Components.interfaces.nsMsgMessageFlags.FeedMsg) ||
-            (aMsgHdr.folder && aMsgHdr.folder.server.type == "rss"));
+    return Boolean(aMsgHdr instanceof Components.interfaces.nsIMsgDBHdr &&
+                   (aMsgHdr.flags & Components.interfaces.nsMsgMessageFlags.FeedMsg ||
+                    this.isFeedFolder(aMsgHdr.folder)));
+  },
+
+  /**
+   * Determine if a folder is a feed acount folder. Trash or a folder in Trash
+   * should be checked with FeedUtils.isInTrash() if required.
+   *
+   * @param nsIMsgFolder aFolder - the folder.
+   *
+   * @return true if folder's server.type is in FeedAccountTypes, false if not.
+   */
+  isFeedFolder: function(aFolder) {
+    return Boolean(aFolder instanceof Components.interfaces.nsIMsgFolder &&
+                   this.FeedAccountTypes.includes(aFolder.server.type));
   },
 
   /**
    * Determine whether to show a feed message summary or load a web page in the
    * message pane.
    *
    * @param nsIMsgDBHdr aMsgHdr - the message.
    * @param bool aToggle        - true if in toggle mode, false otherwise.
--- a/mailnews/extensions/newsblog/jar.mn
+++ b/mailnews/extensions/newsblog/jar.mn
@@ -3,14 +3,14 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 newsblog.jar:
 % content messenger-newsblog %content/messenger-newsblog/
    content/messenger-newsblog/newsblogOverlay.js                (content/newsblogOverlay.js)
    content/messenger-newsblog/Feed.js                           (content/Feed.js)
    content/messenger-newsblog/FeedItem.js                       (content/FeedItem.js)
    content/messenger-newsblog/feed-parser.js                    (content/feed-parser.js)
-*  content/messenger-newsblog/feed-subscriptions.js             (content/feed-subscriptions.js)
+   content/messenger-newsblog/feed-subscriptions.js             (content/feed-subscriptions.js)
    content/messenger-newsblog/feed-subscriptions.xul            (content/feed-subscriptions.xul)
    content/messenger-newsblog/am-newsblog.js                    (content/am-newsblog.js)
    content/messenger-newsblog/am-newsblog.xul                   (content/am-newsblog.xul)
    content/messenger-newsblog/feedAccountWizard.js              (content/feedAccountWizard.js)
    content/messenger-newsblog/feedAccountWizard.xul             (content/feedAccountWizard.xul)
--- a/suite/locales/en-US/chrome/mailnews/newsblog/am-newsblog.dtd
+++ b/suite/locales/en-US/chrome/mailnews/newsblog/am-newsblog.dtd
@@ -1,22 +1,19 @@
 <!-- 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/. -->
 
-<!ENTITY biffStart.label "Check for new articles every ">
-<!ENTITY biffStart.accesskey "k">
-<!ENTITY biffEnd.label "minutes">
-
-<!ENTITY loginAtStartup.label "Check for new articles at startup"> 
+<!ENTITY loginAtStartup.label "Check for new articles at startup">
 <!ENTITY loginAtStartup.accesskey "C">
+<!ENTITY biffAll.label "Enable updates for all feeds">
+<!ENTITY biffAll.accesskey "E">
 
-<!ENTITY useQuickMode.label "By default, show the article summary instead of loading the web page"> 
-<!ENTITY useQuickMode.accesskey "B">
+<!ENTITY newFeedSettings.label "Default Settings for New Feeds">
 
-<!ENTITY manageSubscriptions.label "Manage Subscriptions…"> 
+<!ENTITY manageSubscriptions.label "Manage Subscriptions…">
 <!ENTITY manageSubscriptions.accesskey "M">
 
 <!-- entities from rss.rdf -->
 <!ENTITY feeds.accountName "Blogs &amp; News Feeds">
 <!ENTITY feeds.wizardShortName "Feeds">
 <!ENTITY feeds.wizardLongName "Blogs &amp; News Feeds">
 <!ENTITY feeds.wizardLongName.accesskey "F">
--- a/suite/locales/en-US/chrome/mailnews/newsblog/feed-subscriptions.dtd
+++ b/suite/locales/en-US/chrome/mailnews/newsblog/feed-subscriptions.dtd
@@ -5,33 +5,43 @@
 <!-- Subscription Dialog -->
 <!ENTITY feedSubscriptions.label     "Feed Subscriptions">
 <!ENTITY learnMore.label             "Learn more about Feeds">
 
 <!ENTITY feedTitle.label             "Title:">
 <!ENTITY feedTitle.accesskey         "T">
 
 <!ENTITY feedLocation.label          "Feed URL:">
-<!ENTITY feedLocation.accesskey      "U">
+<!ENTITY feedLocation.accesskey      "F">
 <!ENTITY feedLocation.placeholder    "Enter a valid feed url to Add">
 <!ENTITY locationValidate.label      "Validate">
 <!ENTITY validateText.label          "Check validation and retrieve a valid url.">
 
 <!ENTITY feedFolder.label            "Store Articles in:">
 <!ENTITY feedFolder.accesskey        "S">
 
+<!-- Account Settings and Subscription Dialog -->
+<!ENTITY biffStart.label             "Check for new articles every ">
+<!ENTITY biffStart.accesskey         "k">
+<!ENTITY biffMinutes.label           "minutes">
+<!ENTITY biffMinutes.accesskey       "n">
+<!ENTITY biffDays.label              "days">
+<!ENTITY biffDays.accesskey          "d">
+<!ENTITY recommendedUnits.label      "Publisher recommends:">
+
 <!ENTITY quickMode.label             "Show the article summary instead of loading the web page">
 <!ENTITY quickMode.accesskey         "h">
 
 <!ENTITY autotagEnable.label         "Automatically create tags from feed &lt;category&gt; names">
-<!ENTITY autotagEnable.accesskey     "c">
+<!ENTITY autotagEnable.accesskey     "o">
 <!ENTITY autotagUsePrefix.label      "Prefix tags with:">
 <!ENTITY autotagUsePrefix.accesskey  "P">
 <!ENTITY autoTagPrefix.placeholder   "Enter a tag prefix">
 
+<!-- Subscription Dialog -->
 <!ENTITY button.addFeed.label        "Add">
 <!ENTITY button.addFeed.accesskey    "A">
 <!ENTITY button.updateFeed.label     "Update">
 <!ENTITY button.updateFeed.accesskey "U">
 <!ENTITY button.removeFeed.label     "Remove">
 <!ENTITY button.removeFeed.accesskey "R">
 <!ENTITY button.importOPML.label     "Import">
 <!ENTITY button.importOPML.accesskey "I">