fix problem archiving messages, especially w/ send and archive, r=standard8, bug 644601
authorDavid Bienvenu <bienvenu@nventure.com>
Tue, 26 Apr 2011 13:21:22 -0700
changeset 7624 092fcebea421db3c1c11de329bcb0f647d426b92
parent 7623 ba5211b37c845896da641e657ba9ed8fad1ff4a4
child 7625 e4d3835347ce65ecc8b8c7dc2866c8e64551088d
push idunknown
push userunknown
push dateunknown
reviewersstandard8, bug
bugs644601
fix problem archiving messages, especially w/ send and archive, r=standard8, bug 644601
mailnews/db/msgdb/src/nsDBFolderInfo.cpp
mailnews/imap/src/nsImapMailFolder.cpp
mailnews/imap/src/nsImapMailFolder.h
mailnews/imap/test/unit/test_imapHighWater.js
--- a/mailnews/db/msgdb/src/nsDBFolderInfo.cpp
+++ b/mailnews/db/msgdb/src/nsDBFolderInfo.cpp
@@ -481,16 +481,46 @@ nsDBFolderInfo::GetFolderDate(PRUint32 *
 NS_IMETHODIMP nsDBFolderInfo::SetFolderDate(PRUint32 folderDate)
 {
   m_folderDate = folderDate;
   return SetUint32PropertyWithToken(m_folderDateColumnToken, folderDate);
 }
 
 NS_IMETHODIMP nsDBFolderInfo::GetHighWater(nsMsgKey *result)
 {
+  // Sanity check highwater - if it gets too big, other code
+  // can fail. Look through last 100 messages to recalculate
+  // the highwater mark.
+  *result = m_highWaterMessageKey;
+  if (m_highWaterMessageKey > 0xFFFFFF00 && m_mdb)
+  {
+    nsCOMPtr <nsISimpleEnumerator> hdrs;
+    nsresult rv = m_mdb->ReverseEnumerateMessages(getter_AddRefs(hdrs));
+    if (NS_FAILED(rv))
+      return rv;
+    PRBool hasMore = PR_FALSE;
+    nsCOMPtr<nsIMsgDBHdr> pHeader;
+    nsMsgKey recalculatedHighWater = 1;
+    PRInt32 i = 0;
+    while(i++ < 100 && NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore))
+              && hasMore)
+    {
+      (void) hdrs->GetNext(getter_AddRefs(pHeader));
+      if (pHeader)
+      {
+        nsMsgKey msgKey;
+        pHeader->GetMessageKey(&msgKey);
+        if (msgKey > recalculatedHighWater)
+          recalculatedHighWater = msgKey;
+      }
+    }
+    NS_ASSERTION(m_highWaterMessageKey >= recalculatedHighWater,
+                 "highwater incorrect");
+    m_highWaterMessageKey = recalculatedHighWater;
+  }
   *result = m_highWaterMessageKey;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsDBFolderInfo::SetExpiredMark(nsMsgKey expiredKey)
 {
   m_expiredMark = expiredKey;
   return SetUint32PropertyWithToken(m_expiredMarkColumnToken, expiredKey);
--- a/mailnews/imap/src/nsImapMailFolder.cpp
+++ b/mailnews/imap/src/nsImapMailFolder.cpp
@@ -6955,16 +6955,36 @@ nsresult nsImapMailFolder::CopyOfflineMs
   {
     PRUint32 resultFlags;
     destHdr->OrFlags(nsMsgMessageFlags::Offline, &resultFlags);
     destHdr->SetOfflineMessageSize(messageSize);
   }
   return rv;
 }
 
+nsresult nsImapMailFolder::FindOpenRange(nsMsgKey &fakeBase, PRUint32 srcCount)
+{
+  nsMsgKey newBase = fakeBase - 1;
+  PRUint32 freeCount = 0;
+  while (freeCount != srcCount && newBase > 0)
+  {
+    PRBool containsKey;
+    if (NS_SUCCEEDED(mDatabase->ContainsKey(newBase, &containsKey))
+        && !containsKey)
+      freeCount++;
+    else
+      freeCount = 0;
+    newBase--;
+  }
+  if (!newBase)
+    return NS_ERROR_FAILURE;
+  fakeBase = newBase;
+  return NS_OK;
+}
+
 // this imap folder is the destination of an offline move/copy.
 // We are either offline, or doing a pseudo-offline delete (where we do an offline
 // delete, load the next message, then playback the offline delete).
 nsresult nsImapMailFolder::CopyMessagesOffline(nsIMsgFolder* srcFolder,
                                nsIArray* messages,
                                PRBool isMove,
                                nsIMsgWindow *msgWindow,
                                nsIMsgCopyServiceListener* listener)
@@ -7013,16 +7033,24 @@ nsresult nsImapMailFolder::CopyMessagesO
       // get the highest key in the dest db, so we can make up our fake keys
       nsMsgKey fakeBase = 1;
       nsCOMPtr <nsIDBFolderInfo> folderInfo;
       rv = mDatabase->GetDBFolderInfo(getter_AddRefs(folderInfo));
       NS_ENSURE_SUCCESS(rv, rv);
       nsMsgKey highWaterMark = nsMsgKey_None;
       folderInfo->GetHighWater(&highWaterMark);
       fakeBase += highWaterMark;
+      nsMsgKey fakeTop = fakeBase + srcCount;
+      // Check that we have enough room for the fake headers. If fakeTop
+      // is <= highWaterMark, we've overflowed.
+      if (fakeTop <= highWaterMark || fakeTop == nsMsgKey_None)
+      {
+        rv = FindOpenRange(fakeBase, srcCount);
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
       // N.B. We must not return out of the for loop - we need the matching 
       // end notifications to be sent.
       // We don't need to acquire the semaphor since this is synchronous
       // on the UI thread but we should check if the offline store is locked.
       PRBool isLocked;
       GetLocked(&isLocked);
       nsCOMPtr <nsIInputStream> inputStream;
       nsCOMPtr<nsIOutputStream> outputStream;
@@ -7299,17 +7327,17 @@ void nsImapMailFolder::SetPendingAttribu
 
   // We'll add spaces at beginning and end so we can search for space-name-space
   nsCString dontPreserveEx(NS_LITERAL_CSTRING(" "));
   dontPreserveEx.Append(dontPreserve);
   dontPreserveEx.AppendLiteral(" ");
 
   // these properties are set as integers below, so don't set them again
   // in the iteration through the properties
-  dontPreserveEx.AppendLiteral("offlineMsgSize msgOffset flags priority ");
+  dontPreserveEx.AppendLiteral("offlineMsgSize msgOffset flags priority pseudoHdr ");
 
   // these fields are either copied separately when the server does not support
   // custom IMAP flags, or managed directly through the flags
   dontPreserveEx.AppendLiteral("keywords label ");
 
   PRUint32 i, count;
 
   rv = messages->GetLength(&count);
--- a/mailnews/imap/src/nsImapMailFolder.h
+++ b/mailnews/imap/src/nsImapMailFolder.h
@@ -348,16 +348,19 @@ public:
 
   static nsresult  AllocateUidStringFromKeys(nsMsgKey *keys, PRUint32 numKeys, nsCString &msgIds);
   static nsresult  BuildIdsAndKeyArray(nsIArray* messages, nsCString& msgIds, nsTArray<nsMsgKey>& keyArray);
 
   // these might end up as an nsIImapMailFolder attribute.
   nsresult SetSupportedUserFlags(PRUint32 userFlags);
   nsresult GetSupportedUserFlags(PRUint32 *userFlags);
 
+  // Find the start of a range of msgKeys that can hold srcCount headers.
+  nsresult FindOpenRange(nsMsgKey &fakeBase, PRUint32 srcCount);
+
 protected:
   // Helper methods
 
   virtual nsresult CreateChildFromURI(const nsCString &uri, nsIMsgFolder **folder);
   void FindKeysToAdd(const nsTArray<nsMsgKey> &existingKeys, nsTArray<nsMsgKey>
     &keysToFetch, PRUint32 &numNewUnread, nsIImapFlagAndUidState *flagState);
   void FindKeysToDelete(const nsTArray<nsMsgKey> &existingKeys, nsTArray<nsMsgKey>
     &keysToFetch, nsIImapFlagAndUidState *flagState, PRUint32 boxFlags);
new file mode 100644
--- /dev/null
+++ b/mailnews/imap/test/unit/test_imapHighWater.js
@@ -0,0 +1,217 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test to ensure that offline imap moves handle extremely high highwater
+ * marks.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/Services.jsm");
+load("../../../resources/logHelper.js");
+load("../../../resources/mailTestUtils.js");
+load("../../../resources/asyncTestUtils.js");
+load("../../../resources/messageGenerator.js");
+
+var gIMAPDaemon, gServer, gIMAPIncomingServer;
+
+var gIMAPInbox;
+var gFolder1, gRootFolder;
+
+var dummyDocShell =
+{
+  getInterface: function (iid) {
+    if (iid.equals(Ci.nsIAuthPrompt)) {
+      return Cc["@mozilla.org/login-manager/prompter;1"]
+               .getService(Ci.nsIAuthPrompt);
+    }
+
+    throw Components.results.NS_ERROR_FAILURE;
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDocShell,
+                                         Ci.nsIInterfaceRequestor])
+}
+
+// Dummy message window that ensures we get prompted for logins.
+var dummyMsgWindow =
+{
+  rootDocShell: dummyDocShell,
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMsgWindow,
+                                         Ci.nsISupportsWeakReference])
+};
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox)
+{
+  // Create the imapMessages and store them on the mailbox
+  messages.forEach(function (message)
+  {
+    let dataUri = Services.io.newURI("data:text/plain;base64," +
+                                      btoa(message.toMessageString()),
+                                     null, null);
+    mailbox.addMessage(new imapMessage(dataUri.spec, mailbox.uidnext++, []));
+  });
+}
+
+function run_test()
+{
+  loadLocalMailAccount();
+
+  /*
+   * Set up an IMAP server.
+   */
+  gIMAPDaemon = new imapDaemon();
+  gServer = makeServer(gIMAPDaemon, "");
+  gIMAPDaemon.createMailbox("folder 1", {subscribed : true});
+  gIMAPIncomingServer = createLocalIMAPServer();
+  gIMAPIncomingServer.maximumConnectionsNumber = 1;
+
+  // We need an identity so that updateFolder doesn't fail
+  let localAccount = MailServices.accounts.createAccount();
+  let identity = MailServices.accounts.createIdentity();
+  localAccount.addIdentity(identity);
+  localAccount.defaultIdentity = identity;
+  localAccount.incomingServer = gLocalIncomingServer;
+  MailServices.accounts.defaultAccount = localAccount;
+
+  // Let's also have another account, using the same identity
+  let imapAccount = MailServices.accounts.createAccount();
+  imapAccount.addIdentity(identity);
+  imapAccount.defaultIdentity = identity;
+  imapAccount.incomingServer = gIMAPIncomingServer;
+
+  // pref tuning: one connection only, turn off notifications
+  Services.prefs.setBoolPref("mail.biff.play_sound", false);
+  Services.prefs.setBoolPref("mail.biff.show_alert", false);
+  Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+  Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+  Services.prefs.setBoolPref("mail.server.default.autosync_offline_stores", false);
+  // Don't prompt about offline download when going offline
+  Services.prefs.setIntPref("offline.download.download_messages", 2);
+  actually_run_test();
+}
+
+function setupFolders() {
+  // make 10 messges
+  let messageGenerator = new MessageGenerator();
+  let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+
+  // build up a list of messages
+  let messages = [];
+  messages = messages.concat(scenarioFactory.directReply(10));
+
+  // Add 10 messages with uids 1-10.
+  let imapInbox = gIMAPDaemon.getMailbox("INBOX")
+  addMessagesToServer(messages, imapInbox);
+  messages = [];
+  messages = messages.concat(messageGenerator.makeMessage());
+  // Add a single message to move target folder.
+  addMessagesToServer(messages, gIMAPDaemon.getMailbox("folder 1"));
+
+  // Get the IMAP inbox...
+  gRootFolder = gIMAPIncomingServer.rootFolder;
+  gIMAPInbox = gRootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox)
+                         .QueryInterface(Ci.nsIMsgImapMailFolder);
+  yield true;
+}
+
+function doMoves() {
+  // update folders to download headers.
+  gIMAPInbox.updateFolderWithListener(null, UrlListener);
+  yield false;
+  gFolder1 = gRootFolder.getChildNamed("folder 1")
+               .QueryInterface(Components.interfaces.nsIMsgImapMailFolder);
+  gFolder1.updateFolderWithListener(null, UrlListener);
+  yield false;
+  // get five messages to move from Inbox to folder 1.
+  let headers1 = Cc["@mozilla.org/array;1"]
+                   .createInstance(Ci.nsIMutableArray);
+  let msgEnumerator = gIMAPInbox.msgDatabase.EnumerateMessages();
+  for (i = 0; i < 5 && msgEnumerator.hasMoreElements(); i++)
+  {
+    let header = msgEnumerator.getNext();
+    if (header instanceof Components.interfaces.nsIMsgDBHdr)
+      headers1.appendElement(header, false);
+  }
+  // this will add dummy headers with keys > 0xffffff80
+  MailServices.copy.CopyMessages(gIMAPInbox, headers1, gFolder1, true,
+                                 CopyListener, dummyMsgWindow, true);
+  yield false;
+  gIMAPInbox.updateFolderWithListener(null, UrlListener);
+  yield false;
+  gFolder1.updateFolderWithListener(dummyMsgWindow, UrlListener);
+  yield false;
+  // Check that playing back offline events gets rid of dummy
+  // headers, and thus highWater is recalculated.
+  do_check_eq(gFolder1.msgDatabase.dBFolderInfo.highWater, 6);
+  headers1 = Cc["@mozilla.org/array;1"]
+                .createInstance(Ci.nsIMutableArray);
+  msgEnumerator = gIMAPInbox.msgDatabase.EnumerateMessages();
+  for (i = 0; i < 5 && msgEnumerator.hasMoreElements(); i++)
+  {
+    let header = msgEnumerator.getNext();
+    if (header instanceof Components.interfaces.nsIMsgDBHdr)
+      headers1.appendElement(header, false);
+  }
+  // Check that CopyMessages will handle having a high highwater mark.
+  // It will thrown an exception if it can't.
+  let msgHdr = gFolder1.msgDatabase.CreateNewHdr(0xfffffffd);
+  gFolder1.msgDatabase.AddNewHdrToDB(msgHdr, false);
+  MailServices.copy.CopyMessages(gIMAPInbox, headers1, gFolder1, true,
+                                 CopyListener, dummyMsgWindow, true);
+  yield false;
+  gFolder1.msgDatabase.DeleteHeader(msgHdr, null, true, false);
+  gIMAPInbox.updateFolderWithListener(null, UrlListener);
+  yield false;
+  // this should clear the dummy headers.
+  gFolder1.updateFolderWithListener(dummyMsgWindow, UrlListener);
+  yield false;
+  do_check_eq(gFolder1.msgDatabase.dBFolderInfo.highWater, 11);
+  yield true;
+}
+
+var UrlListener =
+{
+  OnStartRunningUrl: function(url) { },
+  OnStopRunningUrl: function(url, rc)
+  {
+    // Check for ok status.
+    do_check_eq(rc, 0);
+    async_driver();
+  }
+};
+
+// nsIMsgCopyServiceListener implementation
+var CopyListener = 
+{
+  OnStartCopy: function() {},
+  OnProgress: function(aProgress, aProgressMax) {},
+  SetMessageKey: function(aKey){},
+  SetMessageId: function(aMessageId) {},
+  OnStopCopy: function(aStatus){
+    do_check_eq(aStatus, 0);
+    async_driver();
+  }
+};
+
+// Definition of tests
+var tests = [
+  setupFolders,
+  doMoves,
+  endTest
+]
+
+function actually_run_test() {
+  async_run_tests(tests);
+}
+
+function endTest()
+{
+  gIMAPIncomingServer.closeCachedConnections();
+  gServer.stop();
+  let thread = gThreadManager.currentThread;
+  while (thread.hasPendingEvents())
+    thread.processNextEvent(true);
+  yield true;
+}