add backend for offline imap store date constraints, r/sr=standard8, 482476
authorDavid Bienvenu <bienvenu@nventure.com>
Sat, 15 Aug 2009 13:16:40 -0700
changeset 3310 f56b51598806acad6770511e1e35da7cae83716a
parent 3309 05871ae7b38f84c478a4a0151e39d933edb2ec99
child 3311 511e4fffc2e88bb2927639c9c7b413880f8fb3be
push idunknown
push userunknown
push dateunknown
bugs482476
add backend for offline imap store date constraints, r/sr=standard8, 482476
mailnews/base/util/nsMsgUtils.cpp
mailnews/base/util/nsMsgUtils.h
mailnews/imap/src/nsAutoSyncManager.cpp
mailnews/imap/src/nsAutoSyncManager.h
mailnews/imap/src/nsAutoSyncState.cpp
mailnews/imap/src/nsImapMailFolder.cpp
mailnews/imap/src/nsImapMailFolder.h
mailnews/imap/test/unit/test_autosync_date_constraints.js
mailnews/mailnews.js
--- a/mailnews/base/util/nsMsgUtils.cpp
+++ b/mailnews/base/util/nsMsgUtils.cpp
@@ -1938,8 +1938,19 @@ NS_MSG_BASE nsresult MsgPromptLoginFaile
   PRBool dummyValue;
   return dialog->ConfirmEx(
     title.get(), message.get(),
     (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
     (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1) +
     (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2),
     button0.get(), nsnull, button2.get(), nsnull, &dummyValue, aResult);
 }
+
+NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(PRInt32 ageInDays)
+{
+  PRInt64 secondsInDays, microSecondsInDay;
+  PRTime now = PR_Now();
+
+  secondsInDays = 60 * 60 * 24 * ageInDays;
+  microSecondsInDay = secondsInDays * PR_USEC_PER_SEC;
+  return now - microSecondsInDay;
+}
+
--- a/mailnews/base/util/nsMsgUtils.h
+++ b/mailnews/base/util/nsMsgUtils.h
@@ -257,9 +257,15 @@ NS_MSG_BASE PRBool MsgAdvanceToNextLine(
  *                   2 for enter a new password.
  * @return           NS_OK for success, NS_ERROR_* if there was a failure in
  *                   creating the dialog.
  */
 NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow *aMsgWindow,
                                           const nsCString &aHostname,
                                           PRInt32 *aResult);
 
+/**
+ * Calculate a PRTime value used to determine if a date is XX
+ * days ago. This is used by various retention setting algorithms.
+ */
+NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(PRInt32 ageInDays);
+
 #endif
--- a/mailnews/imap/src/nsAutoSyncManager.cpp
+++ b/mailnews/imap/src/nsAutoSyncManager.cpp
@@ -43,24 +43,31 @@
 #include "nsIMsgMailNewsUrl.h"
 #include "nsIMsgAccountManager.h"
 #include "nsIMsgIncomingServer.h"
 #include "nsIMsgMailSession.h"
 #include "nsMsgFolderFlags.h"
 #include "nsImapIncomingServer.h"
 #include "nsMsgUtils.h"
 #include "nsIIOService.h"
+#include "nsIPrefService.h"
 
 NS_IMPL_ISUPPORTS1(nsDefaultAutoSyncMsgStrategy, nsIAutoSyncMsgStrategy)
 
 const char* kAppIdleNotification = "mail:appIdle";
 const char* kStartupDoneNotification = "mail-startup-done";
 
 nsDefaultAutoSyncMsgStrategy::nsDefaultAutoSyncMsgStrategy()
 {
+  m_offlineMsgAgeLimit = -1;
+
+  // Check if we've limited the offline storage by age.
+  nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+  if (prefBranch)
+    prefBranch->GetIntPref("mail.autosync.max_age_days", &m_offlineMsgAgeLimit);
 }
 
 nsDefaultAutoSyncMsgStrategy::~nsDefaultAutoSyncMsgStrategy()
 {
 }
 
 NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::Sort(nsIMsgFolder *aFolder, 
   nsIMsgDBHdr *aMsgHdr1, nsIMsgDBHdr *aMsgHdr2, nsAutoSyncStrategyDecisionType *aDecision)
@@ -110,17 +117,21 @@ NS_IMETHODIMP nsDefaultAutoSyncMsgStrate
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP nsDefaultAutoSyncMsgStrategy::IsExcluded(nsIMsgFolder *aFolder, 
   nsIMsgDBHdr *aMsgHdr, PRBool *aDecision)
 {
   NS_ENSURE_ARG_POINTER(aDecision);
-  *aDecision = PR_FALSE;  
+  NS_ENSURE_ARG_POINTER(aMsgHdr);
+  PRInt64 msgDate;
+  aMsgHdr->GetDate(&msgDate);
+  *aDecision = m_offlineMsgAgeLimit > 0 &&
+    msgDate < MsgConvertAgeInDaysToCutoffDate(m_offlineMsgAgeLimit);
   return NS_OK;
 }
 
 NS_IMPL_ISUPPORTS1(nsDefaultAutoSyncFolderStrategy, nsIAutoSyncFolderStrategy)
 
 nsDefaultAutoSyncFolderStrategy::nsDefaultAutoSyncFolderStrategy()
 {
 }
--- a/mailnews/imap/src/nsAutoSyncManager.h
+++ b/mailnews/imap/src/nsAutoSyncManager.h
@@ -108,25 +108,26 @@ class nsIMsgFolder;
  */
  
 /**
  * Default strategy implementation to prioritize messages in the download queue.   
  */
 class nsDefaultAutoSyncMsgStrategy : public nsIAutoSyncMsgStrategy
 {
   static const PRUint32 kFirstPassMessageSize = 60U*1024U; // 60K
-  
+
   public:
     NS_DECL_ISUPPORTS
     NS_DECL_NSIAUTOSYNCMSGSTRATEGY
 
     nsDefaultAutoSyncMsgStrategy();
 
   private:
     ~nsDefaultAutoSyncMsgStrategy();
+    PRInt32 m_offlineMsgAgeLimit;
 };
 
 /**
  * Default strategy implementation to prioritize folders in the download queue.  
  */
 class nsDefaultAutoSyncFolderStrategy : public nsIAutoSyncFolderStrategy
 {
   public:
--- a/mailnews/imap/src/nsAutoSyncState.cpp
+++ b/mailnews/imap/src/nsAutoSyncState.cpp
@@ -94,23 +94,23 @@ PRBool MsgStrategyComparatorAdaptor::Les
   
   mDatabase->GetMsgHdrForKey(a, getter_AddRefs(hdrA));
   mDatabase->GetMsgHdrForKey(b, getter_AddRefs(hdrB));
 
   if (hdrA && hdrB)
   {
     nsresult rv;
     nsAutoSyncStrategyDecisionType decision = nsAutoSyncStrategyDecisions::Same;
-    
+
     nsCOMPtr<nsIMsgFolder> folder = do_QueryInterface(mFolder);
     if (mStrategy)
       rv = mStrategy->Sort(folder, hdrA, hdrB, &decision);
-      
+
     if (NS_SUCCEEDED(rv))
-        return (decision == nsAutoSyncStrategyDecisions::Lower);      
+      return (decision == nsAutoSyncStrategyDecisions::Lower);
   }
   
   return PR_FALSE;
 }
 
 nsAutoSyncState::nsAutoSyncState(nsImapMailFolder *aOwnerFolder, PRTime aLastSyncTime)
   : mSyncState(stCompletedIdle), mOffset(0U), mLastOffset(0U), mLastServerTotal(0),
     mLastServerRecent(0), mLastServerUnseen(0), mLastNextUID(0),
--- a/mailnews/imap/src/nsImapMailFolder.cpp
+++ b/mailnews/imap/src/nsImapMailFolder.cpp
@@ -1193,16 +1193,69 @@ NS_IMETHODIMP nsImapMailFolder::SetExpli
 }
 
 NS_IMETHODIMP nsImapMailFolder::GetNoSelect(PRBool *aResult)
 {
   NS_ENSURE_ARG_POINTER(aResult);
   return GetFlag(nsMsgFolderFlags::ImapNoselect, aResult);
 }
 
+NS_IMETHODIMP nsImapMailFolder::ApplyRetentionSettings()
+{
+  PRInt32 numDaysToKeepOfflineMsgs = -1;
+
+  // Check if we've limited the offline storage by age.
+  nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+  if (prefBranch)
+    prefBranch->GetIntPref("mail.autosync.max_age_days", &numDaysToKeepOfflineMsgs);
+
+  nsCOMPtr<nsIMsgDatabase> holdDBOpen;
+  if (numDaysToKeepOfflineMsgs > 0)
+  {
+    PRBool dbWasCached = mDatabase != nsnull;
+    nsresult rv = GetDatabase();
+    NS_ENSURE_SUCCESS(rv, rv);
+    nsCOMPtr <nsISimpleEnumerator> hdrs;
+    rv = mDatabase->EnumerateMessages(getter_AddRefs(hdrs));
+    NS_ENSURE_SUCCESS(rv, rv);
+    PRBool hasMore = PR_FALSE;
+
+    PRTime cutOffDay =
+      MsgConvertAgeInDaysToCutoffDate(numDaysToKeepOfflineMsgs);
+
+    nsCOMPtr <nsIMsgDBHdr> pHeader;
+    // so now cutOffDay is the PRTime cut-off point. Any offline msg with 
+    // a date less than that will get marked for pending removal.
+    while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore)
+    {
+      rv = hdrs->GetNext(getter_AddRefs(pHeader));
+      NS_ENSURE_SUCCESS(rv, rv);
+      PRUint32 msgFlags;
+      PRTime msgDate;
+      pHeader->GetFlags(&msgFlags);
+      if (msgFlags & nsMsgMessageFlags::Offline)
+      {
+        pHeader->GetDate(&msgDate);
+        if (msgDate < cutOffDay)
+          MarkPendingRemoval(pHeader);
+        // I'm horribly tempted to break out of the loop if we've found
+        // a message after the cut-off date, because messages will most likely
+        // be in date order in the db, but there are always edge cases.
+      }
+    }
+    if (!dbWasCached)
+    {
+      holdDBOpen = mDatabase;
+      mDatabase = nsnull;
+    }
+  }
+  return nsMsgDBFolder::ApplyRetentionSettings();
+}
+
+
 NS_IMETHODIMP nsImapMailFolder::Compact(nsIUrlListener *aListener, nsIMsgWindow *aMsgWindow)
 {
   nsresult rv = GetDatabase();
   // now's a good time to apply the retention settings. If we do delete any
   // messages, the expunge is going to have to wait until the delete to
   // finish before it can run, but the multiple-connection protection code
   // should handle that.
   if (mDatabase)
--- a/mailnews/imap/src/nsImapMailFolder.h
+++ b/mailnews/imap/src/nsImapMailFolder.h
@@ -257,16 +257,18 @@ public:
   NS_IMETHOD GetDeletable (PRBool *deletable);
   NS_IMETHOD GetRequiresCleanup(PRBool *requiresCleanup);
 
   NS_IMETHOD GetSizeOnDisk(PRUint32 * size);
 
   NS_IMETHOD GetCanCreateSubfolders(PRBool *aResult);
   NS_IMETHOD GetCanSubscribe(PRBool *aResult);
 
+  NS_IMETHOD ApplyRetentionSettings();
+
   NS_IMETHOD AddMessageDispositionState(nsIMsgDBHdr *aMessage, nsMsgDispositionState aDispositionFlag);
   NS_IMETHOD MarkMessagesRead(nsIArray *messages, PRBool markRead);
   NS_IMETHOD MarkAllMessagesRead(nsIMsgWindow *aMsgWindow);
   NS_IMETHOD MarkMessagesFlagged(nsIArray *messages, PRBool markFlagged);
   NS_IMETHOD MarkThreadRead(nsIMsgThread *thread);
   NS_IMETHOD SetLabelForMessages(nsIArray *aMessages, nsMsgLabelValue aLabel);
   NS_IMETHOD SetJunkScoreForMessages(nsIArray *aMessages, const nsACString& aJunkScore);
   NS_IMETHOD DeleteSubFolders(nsIArray *folders, nsIMsgWindow *msgWindow);
new file mode 100644
--- /dev/null
+++ b/mailnews/imap/test/unit/test_autosync_date_constraints.js
@@ -0,0 +1,188 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test autosync date constraints
+ */
+
+var gIMAPDaemon, gServer, gIMAPIncomingServer;
+
+load("../../mailnews/resources/messageGenerator.js");
+load("../../mailnews/resources/asyncTestUtils.js");
+
+const gIMAPService = Cc["@mozilla.org/messenger/messageservice;1?type=imap"]
+                       .getService(Ci.nsIMsgMessageService);
+
+// Globals
+var gRootFolder;
+var gIMAPInbox, gIMAPTrashFolder, gMsgImapInboxFolder;
+var gIMAPDaemon, gServer, gIMAPIncomingServer;
+var gImapInboxOfflineStoreSize;
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox, localFolder)
+{
+  // Create the imapMessages and store them on the mailbox
+  messages.forEach(function (message)
+  {
+    let dataUri = 'data:text/plain,' + message.toMessageString();
+    mailbox.addMessage(new imapMessage(dataUri, mailbox.uidnext++, []));
+  });
+}
+
+const gTestArray =
+[
+  function downloadForOffline() {
+    // ...and download for offline use.
+    // This downloads all messages, ignoring the autosync age constraints.
+    gIMAPInbox.downloadAllForOffline(UrlListener, null);
+  },
+  function applyRetentionSettings() {
+    gIMAPInbox.applyRetentionSettings();
+    let enumerator = gIMAPInbox.msgDatabase.EnumerateMessages();
+    if (enumerator) {
+      let now = new Date();
+      let dateInSeconds = now.getSeconds();
+      let cutOffDateInSeconds = dateInSeconds - (5 * 60 * 24);
+      while (enumerator.hasMoreElements()) {
+        let header = enumerator.getNext();
+        if (header instanceof Components.interfaces.nsIMsgDBHdr) {
+          if (header.dateInSeconds < cutOffDateInSeconds)
+            do_check_eq(header.getStringProperty("pendingRemoval"), "1");
+          else
+            do_check_eq(header.getStringProperty("pendingRemoval"), "");
+        }
+      }
+    }
+    doTest(++gCurTestNum);
+  }
+];
+
+function run_test()
+{
+  // This is before any of the actual tests, so...
+  gTest = 0;
+
+  // Add a listener.
+  gIMAPDaemon = new imapDaemon();
+  gServer = makeServer(gIMAPDaemon, "");
+
+  gIMAPIncomingServer = createLocalIMAPServer();
+
+  loadLocalMailAccount();
+
+  // We need an identity so that updateFolder doesn't fail
+  let acctMgr = Cc["@mozilla.org/messenger/account-manager;1"]
+                  .getService(Ci.nsIMsgAccountManager);
+  let localAccount = acctMgr.createAccount();
+  let identity = acctMgr.createIdentity();
+  localAccount.addIdentity(identity);
+  localAccount.defaultIdentity = identity;
+  localAccount.incomingServer = gLocalIncomingServer;
+  acctMgr.defaultAccount = localAccount;
+
+  // Let's also have another account, using the same identity
+  let imapAccount = acctMgr.createAccount();
+  imapAccount.addIdentity(identity);
+  imapAccount.defaultIdentity = identity;
+  imapAccount.incomingServer = gIMAPIncomingServer;
+
+  // The server doesn't support more than one connection
+  let prefBranch = Cc["@mozilla.org/preferences-service;1"]
+                     .getService(Ci.nsIPrefBranch);
+  prefBranch.setIntPref("mail.server.server1.max_cached_connections", 1);
+  // Make sure no biff notifications happen
+  prefBranch.setBoolPref("mail.biff.play_sound", false);
+  prefBranch.setBoolPref("mail.biff.show_alert", false);
+  prefBranch.setBoolPref("mail.biff.show_tray_icon", false);
+  prefBranch.setBoolPref("mail.biff.animate_dock_icon", false);
+  // We aren't interested in downloading messages automatically
+  prefBranch.setBoolPref("mail.server.server1.download_on_biff", false);
+  prefBranch.setIntPref("mail.autosync.max_age_days", 4);
+
+  // Get the server list...
+  gIMAPIncomingServer.performExpand(null);
+
+  gRootFolder = gIMAPIncomingServer.rootFolder;
+  gIMAPInbox = gRootFolder.getChildNamed("INBOX");
+  gMsgImapInboxFolder = gIMAPInbox.QueryInterface(Ci.nsIMsgImapMailFolder);
+  // these hacks are required because we've created the inbox before
+  // running initial folder discovery, and adding the folder bails
+  // out before we set it as verified online, so we bail out, and
+  // then remove the INBOX folder since it's not verified.
+  gMsgImapInboxFolder.hierarchyDelimiter = '/';
+  gMsgImapInboxFolder.verifiedAsOnlineFolder = true;
+
+
+  // Add a couple of messages to the INBOX
+  // this is synchronous, afaik
+  gMessageGenerator = new MessageGenerator();
+  gScenarioFactory = new MessageScenarioFactory(gMessageGenerator);
+
+  // build up a diverse list of messages
+  let messages = [];
+  messages = messages.concat(gMessageGenerator.makeMessage({age: {days: 2, hours: 1}}));
+  messages = messages.concat(gMessageGenerator.makeMessage({age: {days: 8, hours: 1}}));
+  messages = messages.concat(gMessageGenerator.makeMessage({age: {days: 10, hours: 1}}));
+
+  addMessagesToServer(messages,
+                      gIMAPDaemon.getMailbox("INBOX"), gIMAPInbox);
+  // "Master" do_test_pending(), paired with a do_test_finished() at the end of
+  // all the operations.
+  do_test_pending();
+  //start first test
+  doTest(1);
+}
+
+function doTest(test)
+{
+  if (test <= gTestArray.length)
+  {
+    dump("Doing test " + test + "\n");
+    gCurTestNum = test;
+
+    var testFn = gTestArray[test-1];
+    // Set a limit of three seconds; if the notifications haven't arrived by then there's a problem.
+    do_timeout(10000, "if (gCurTestNum == "+test+") \
+      do_throw('Notifications not received in 10000 ms for operation "+testFn.name+", current status is '+gCurrStatus);");
+    try {
+    testFn();
+    } catch(ex) {dump(ex);}
+  }
+  else
+  {
+    // Cleanup, null out everything, close all cached connections and stop the
+    // server
+    gRootFolder = null;
+    gIMAPInbox = null;
+    gMsgImapFolder = null;
+    gIMAPTrashFolder = null;
+    do_timeout_function(1000, endTest);
+  }
+}
+
+function endTest()
+{
+  gServer.resetTest();
+  gIMAPIncomingServer.closeCachedConnections();
+  gServer.performTest();
+  gServer.stop();
+  let thread = gThreadManager.currentThread;
+  while (thread.hasPendingEvents())
+    thread.processNextEvent(true);
+
+  do_test_finished(); // for the one in run_test()
+}
+
+var UrlListener = 
+{
+  OnStartRunningUrl: function(url) { },
+
+  OnStopRunningUrl: function (aUrl, aExitCode) {
+    // Check: message successfully copied.
+    do_check_eq(aExitCode, 0);
+    // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+    // This can happen with a bunch of synchronous functions grouped together, and
+    // can even cause tests to fail because they're still waiting for the listener
+    // to return
+    do_timeout(0, "doTest(++gCurTestNum)");
+  }
+};
--- a/mailnews/mailnews.js
+++ b/mailnews/mailnews.js
@@ -522,16 +522,19 @@ pref("mail.server.default.purgeSpamInter
 pref("mail.server.default.inhibitWhiteListingIdentityUser", true);
 // should we inhibit whitelisting of the domain for a server's identities?
 pref("mail.server.default.inhibitWhiteListingIdentityDomain", false);
 
 // to activate auto-sync feature (preemptive message download for imap) by default
 pref("mail.server.default.autosync_offline_stores",true);
 pref("mail.server.default.offline_download",true);
 
+// -1 means no limit, no purging of offline stores.
+pref("mail.autosync.max_age_days", -1);
+
 pref("mail.server.default.archive_granularity", 1);
 // the probablilty threshold over which messages are classified as junk
 // this number is divided by 100 before it is used. The classifier can be fine tuned
 // by changing this pref. Typical values are .99, .95, .90, .5, etc.
 pref("mail.adaptivefilters.junk_threshold", 90);
 pref("mail.spam.version", 0); // used to determine when to migrate global spam settings
 pref("mail.spam.logging.enabled", false);
 pref("mail.spam.manualMark", false);