keep imap offline stores up to date when messages are moved/copied (including fcc/save as draft), r=asuth, sr=standard8 bug 574441
authorDavid Bienvenu <bienvenu@nventure.com>
Wed, 19 Jan 2011 12:32:50 -0800
changeset 6984 7cad2968cbb72b7d27d0d883630b8029373b668a
parent 6983 c903a7fb5e32da8453142ef6b25a08370d330b4b
child 6985 fefbbf03688aa4c322f3630c072c27f27c9c9f1d
push idunknown
push userunknown
push dateunknown
reviewersasuth, standard8
bugs574441
keep imap offline stores up to date when messages are moved/copied (including fcc/save as draft), r=asuth, sr=standard8 bug 574441
mailnews/base/public/nsIMsgFolderListener.idl
mailnews/base/public/nsIMsgFolderNotificationService.idl
mailnews/base/src/nsMsgFolderNotificationService.cpp
mailnews/base/test/unit/test_nsIMsgFolderListener.js
mailnews/imap/public/nsIMsgImapMailFolder.idl
mailnews/imap/src/nsImapIncomingServer.cpp
mailnews/imap/src/nsImapMailFolder.cpp
mailnews/imap/src/nsImapMailFolder.h
mailnews/imap/src/nsImapOfflineSync.cpp
mailnews/imap/test/unit/test_imapAttachmentSaves.js
mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js
mailnews/local/src/nsRssIncomingServer.cpp
mailnews/test/resources/msgFolderListenerSetup.js
--- a/mailnews/base/public/nsIMsgFolderListener.idl
+++ b/mailnews/base/public/nsIMsgFolderListener.idl
@@ -31,32 +31,33 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 #include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
 
 interface nsIMsgDBHdr;
 interface nsIMsgFolder;
 interface nsIArray;
 
 /**
  * This is similar to nsIFolderListener, but with slightly different semantics,
  * especially w.r.t. moving messages and folders.  Some listeners want to know
  * about moves, instead of getting an itemAdded and itemRemoved notification.
  * Folder listeners also only tend to get called if a view is open on the folder,
  * which is not always the case. I don't want to change nsIFolderListener at this
  * point since there are lots of extensions that rely on it. Eventually,
  * these two interfaces should be combined somehow.
  */
 
-[scriptable, uuid(63533aa2-9e03-424c-b308-a3ee5ad47bc5)]
+[scriptable, uuid(2f87be72-0565-4e64-a824-0eb9c258f884)]
 interface nsIMsgFolderListener : nsISupports {
   /**
    * Notified immediately after a message is added to a folder. This could be a
    * new incoming message to a local folder, or a new message in an IMAP folder
    * when it is opened.
    *
    * You may want to consider using the msgsClassified notification instead of
    * this notification if any of the following are true:
@@ -119,30 +120,51 @@ interface nsIMsgFolderListener : nsISupp
   /**
    * Notified after a command to move or copy a group of messages completes. In
    * case of a move, this is before the messages have been deleted from the
    * source folder.
    *
    * @param aMove true if a move, false if a copy
    * @param aSrcMsgs An array of the message headers in the source folder
    * @param aDestFolder The folder these messages were moved to.
-   * @param aDestMsgs Present only for local folder moves, it provides the list
-   *     of target message headers.
+   * @param aDestMsgs This provides the list of target message headers.
+                      For imap messages, these will be "pseudo" headers, with
+                      a made up UID. When we download the "real" header, we
+                      will send a msgKeyChanged notification. Currently, if
+                      the imap move/copy happens strictly online (essentially,
+                      not user-initiated), then aDestMsgs will be null.
    *
    * @note
    * If messages are moved from a server which uses the IMAP delete model,
    * you'll get aMove = false. That's because the messages are not deleted from
    * the source database, but instead simply marked deleted.
    */
   void msgsMoveCopyCompleted(in boolean aMove,
                              in nsIArray aSrcMsgs,
                              in nsIMsgFolder aDestFolder,
                              in nsIArray aDestMsgs);
 
   /**
+   * Notification sent when the msg key for a header may have changed.
+   * This is used when we create a header for an offline imap move result,
+   * without knowing what the ultimate UID will be. When we download the
+   * headers for the new message, we replace the old "pseudo" header with
+   * a new header that has the correct UID/message key. The uid of the new hdr
+   * may turn out to be the same as aOldKey if we've guessed correctly but
+   * the listener can use this notification to know that it can ignore the
+   * msgAdded notification that's coming for aNewHdr. We do NOT send a
+   * msgsDeleted notification for the pseudo header.
+   *
+   * @param aOldKey The fake UID. The header with this key has been removed
+   *                by the time this is called.
+   * @param aNewHdr The header that replaces the header with aOldKey.
+   */
+  void msgKeyChanged(in nsMsgKey aOldKey, in nsIMsgDBHdr aNewHdr);
+
+  /**
    * Notified after a folder has been added.
    *
    * @param aFolder The folder that has just been added
    */
   void folderAdded(in nsIMsgFolder aFolder);
 
   /**
    * Notified after a folder has been deleted and its corresponding file(s) deleted from disk.
--- a/mailnews/base/public/nsIMsgFolderNotificationService.idl
+++ b/mailnews/base/public/nsIMsgFolderNotificationService.idl
@@ -31,25 +31,26 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 #include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
 
 interface nsIMsgDBHdr;
 interface nsIMsgFolder;
 interface nsIMsgFolderListener;
 interface nsIArray;
 
 typedef unsigned long msgFolderListenerFlag;
 
-[scriptable, uuid(45420c55-b460-4a61-bd67-7f763a69a46e)]
+[scriptable, uuid(e54a592c-2f23-4771-9670-bdb9d4f5dbbd)]
 interface nsIMsgFolderNotificationService : nsISupports {
   /**
    * @name Notification flags
    * These flags determine which notifications will be sent.
    * @{
    */
   /// nsIMsgFolderListener::msgAdded notification
   const msgFolderListenerFlag msgAdded = 0x1;
@@ -72,16 +73,20 @@ interface nsIMsgFolderNotificationServic
   /// nsIMsgFolderListener::folderMoveCopyCompleted notification
   const msgFolderListenerFlag folderMoveCopyCompleted = 0x2000;
 
   /// nsIMsgFolderListener::folderRenamed notification
   const msgFolderListenerFlag folderRenamed = 0x4000;
 
   /// nsIMsgFolderListener::itemEvent notification
   const msgFolderListenerFlag itemEvent = 0x1000000;
+
+  /// nsIMsgFolderListener::msgKeyChanged notification
+  const msgFolderListenerFlag msgKeyChanged = 0x2000000;
+
   /** @} */
 
   readonly attribute boolean hasListeners;
   void addListener(in nsIMsgFolderListener aListener,
                    in msgFolderListenerFlag flags);
   void removeListener(in nsIMsgFolderListener aListener);
 
   // message-specific functions
@@ -91,16 +96,29 @@ interface nsIMsgFolderNotificationServic
                             in boolean aJunkProcessed,
                             in boolean aTraitProcessed);
   void notifyMsgsDeleted(in nsIArray aMsgs);
   void notifyMsgsMoveCopyCompleted(in boolean aMove, 
                                    in nsIArray aSrcMsgs, 
                                    in nsIMsgFolder aDestFolder,
                                    in nsIArray aDestMsgs);
 
+  /**
+   * Notify listeners that the msg key for a header has changed. Currently,
+   * this is used when we create a header for an offline imap move result,
+   * without knowing what the ultimate UID will be. When we download the
+   * headers for the new message, we replace the old "pseudo" header with
+   * a new header that has the correct UID/message key, by cloning the pseudo
+   * header, which maintains all the existing header attributes.
+   *
+   * @param aOldKey The fake UID. The header with this key has been removed
+   *                by the time this is called.
+   * @param aNewHdr The header that replaces the header with aOldKey.
+   */
+  void notifyMsgKeyChanged(in nsMsgKey aOldKey, in nsIMsgDBHdr aNewHdr);
   // folder specific functions
   // single folders, all the time
   void notifyFolderAdded(in nsIMsgFolder aFolder);
   void notifyFolderDeleted(in nsIMsgFolder aFolder);
   void notifyFolderMoveCopyCompleted(in boolean aMove,
                                      in nsIMsgFolder aSrcFolder,
                                      in nsIMsgFolder aDestFolder);
   void notifyFolderRenamed(in nsIMsgFolder aOrigFolder,
--- a/mailnews/base/src/nsMsgFolderNotificationService.cpp
+++ b/mailnews/base/src/nsMsgFolderNotificationService.cpp
@@ -160,16 +160,24 @@ NS_IMETHODIMP nsMsgFolderNotificationSer
     }
   }
 
   NOTIFY_MSGFOLDER_LISTENERS(msgsMoveCopyCompleted, MsgsMoveCopyCompleted,
                              (isReallyMove, aSrcMsgs, aDestFolder, aDestMsgs));
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsMsgFolderNotificationService::NotifyMsgKeyChanged(nsMsgKey aOldKey,
+                                                    nsIMsgDBHdr *aNewHdr)
+{
+  NOTIFY_MSGFOLDER_LISTENERS(msgKeyChanged, MsgKeyChanged, (aOldKey, aNewHdr));
+  return NS_OK;
+}
+
 /* void notifyFolderAdded(in nsIMsgFolder aFolder); */
 NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderAdded(nsIMsgFolder *aFolder)
 {
   NOTIFY_MSGFOLDER_LISTENERS(folderAdded, FolderAdded, (aFolder));
   return NS_OK;
 }
 
 /* void notifyFolderDeleted(in nsIMsgFolder aFolder); */
--- a/mailnews/base/test/unit/test_nsIMsgFolderListener.js
+++ b/mailnews/base/test/unit/test_nsIMsgFolderListener.js
@@ -16,32 +16,32 @@ const nsIMFListener = Ci.nsIMsgFolderLis
 
 
 const gIndividualFlags =
 [
   nsIMFNService.msgAdded,
   nsIMFNService.msgsClassified,
   nsIMFNService.msgsDeleted,
   nsIMFNService.msgsMoveCopyCompleted,
+  nsIMFNService.msgKeyChanged,
   nsIMFNService.folderAdded,
   nsIMFNService.folderDeleted,
   nsIMFNService.folderMoveCopyCompleted,
   nsIMFNService.folderRenamed,
   nsIMFNService.itemEvent,
 ];
 
 var gMFNService = Cc[mFNSContractID].getService(nsIMFNService);
 
 // Our listener, which captures events.
 function gMFListener() {}
 gMFListener.prototype =
 {
   mReceived: 0,
   mRemoveSelf: false,
-
   msgAdded: function (aMsg)
   {
     do_check_eq(this.mReceived & nsIMFNService.msgAdded, 0);
     this.mReceived |= nsIMFNService.msgAdded;
     if (this.mRemoveSelf)
       gMFNService.removeListener(this);
   },
 
@@ -63,17 +63,25 @@ gMFListener.prototype =
 
   msgsMoveCopyCompleted: function (aMove, aSrcMsgs, aDestFolder, aDestMsgs)
   {
     do_check_eq(this.mReceived & nsIMFNService.msgsMoveCopyCompleted, 0);
     this.mReceived |= nsIMFNService.msgsMoveCopyCompleted;
     if (this.mRemoveSelf)
       gMFNService.removeListener(this);
   },
-  
+
+  msgKeyChanged: function(aOldMsgKey, aNewMsgHdr)
+  {
+    do_check_eq(this.mReceived & nsIMFNService.msgKeyChanged, 0);
+    this.mReceived |= nsIMFNService.msgKeyChanged;
+    if (this.mRemoveSelf)
+      gMFNService.removeListener(this);
+  },
+
   folderAdded: function (aFolder)
   {
     do_check_eq(this.mReceived & nsIMFNService.folderAdded, 0);
     this.mReceived |= nsIMFNService.folderAdded;
     if (this.mRemoveSelf)
       gMFNService.removeListener(this);
   },
 
@@ -111,16 +119,17 @@ gMFListener.prototype =
 };
 
 function NotifyMsgFolderListeners()
 {
   gMFNService.notifyMsgAdded(null);
   gMFNService.notifyMsgsClassified(null, null, null);
   gMFNService.notifyMsgsDeleted(null);
   gMFNService.notifyMsgsMoveCopyCompleted(null, null, null, null);
+  gMFNService.notifyMsgKeyChanged(null, null);
   gMFNService.notifyFolderAdded(null);
   gMFNService.notifyFolderDeleted(null);
   gMFNService.notifyFolderMoveCopyCompleted(null, null, null);
   gMFNService.notifyFolderRenamed(null, null);
   gMFNService.notifyItemEvent(null, null, null);
 }
 
 function run_test()
--- a/mailnews/imap/public/nsIMsgImapMailFolder.idl
+++ b/mailnews/imap/public/nsIMsgImapMailFolder.idl
@@ -74,17 +74,17 @@ interface nsIMsgImapFolderProps : nsISup
     void setQuotaStatus(in AString folderQuotaStatus);
 
     /**
      * Updates the quota data displayed in the Quota tab.
      */
     void setQuotaData(in ACString quotaroot, in unsigned long usedKB, in unsigned long maxKB);
 };
 
-[scriptable, uuid(d33884c6-20d5-4a84-880c-02ac56c1119f)]
+[scriptable, uuid(fea0f455-7adf-4683-bf2f-c95c3fff03df)]
 interface nsIMsgImapMailFolder : nsISupports {
   void removeSubFolder(in nsIMsgFolder folder);
   void createClientSubfolderInfo(in ACString folderName, in char hierarchyDelimiter,
                                  in long flags, in boolean suppressNotification);
   void list();
   void renameLocal(in ACString newname, in nsIMsgFolder parent);
   void prepareToRename();
   void performExpand(in nsIMsgWindow aMsgWindow);
@@ -93,17 +93,26 @@ interface nsIMsgImapMailFolder : nsISupp
 
   // these are used for offline synchronization
   void storeImapFlags(in long aFlags, in boolean aAddFlags, [array, size_is (aNumKeys)] 
       in nsMsgKey aKeysToFlag, in unsigned long aNumKeys, in nsIUrlListener aUrlListener);
   nsIURI setImapFlags(in string uids, in long flags);
   void replayOfflineMoveCopy([array, size_is (numKeys)] in nsMsgKey keys, in unsigned long numKeys, in boolean isMove, in nsIMsgFolder aDstFolder,
                          in nsIUrlListener aUrlListener, in nsIMsgWindow aWindow);
   nsIURI playbackOfflineFolderCreate(in AString folderName, in nsIMsgWindow aWindow);
-
+  /**
+   * This is called by the offline sync code to tell the imap folder to
+   * remember info about the header with this key (messageId and key) because
+   * it's an offline move result header, and we need to generate an
+   * nsIMsgFolderListener.msgKeyChanged notification when we download the
+   * real header from the imap server.
+   *
+   * @param aMsgKey msg key of move result pseudo hdr.
+   */
+  void addMoveResultPseudoKey(in nsMsgKey aMsgKey);
   /**
    * Select this folder on the imap server without doing a sync of flags or
    * headers. This is used for offline playback, where we don't want to
    * download hdrs we don't have, because they may have been offline deleted.
    *
    * @param aUrlListener        url listener, can be null
    * @param aWindow          msg window url is running in, can be null
    */
--- a/mailnews/imap/src/nsImapIncomingServer.cpp
+++ b/mailnews/imap/src/nsImapIncomingServer.cpp
@@ -859,18 +859,27 @@ nsImapIncomingServer::CreateProtocolInst
       break;
   }
   nsIImapProtocol * protocolInstance;
   rv = CallCreateInstance(kImapProtocolCID, &protocolInstance);
   if (NS_SUCCEEDED(rv) && protocolInstance)
   {
     nsCOMPtr<nsIImapHostSessionList> hostSession =
       do_GetService(kCImapHostSessionListCID, &rv);
-    if (NS_SUCCEEDED(rv))
-      rv = protocolInstance->Initialize(hostSession, this, aEventTarget);
+    NS_ENSURE_SUCCESS(rv, rv);
+    PRInt32 socketType;
+    if (NS_SUCCEEDED(GetSocketType(&socketType)) &&
+        socketType == nsMsgSocketType::trySTARTTLS)
+    {
+      PRUint32 capability = kCapabilityUndefined;
+      hostSession->GetCapabilityForHost(m_serverKey.get(), capability);
+      if (capability & kHasStartTLSCapability)
+        SetSocketType(nsMsgSocketType::alwaysSTARTTLS);
+    }
+    rv = protocolInstance->Initialize(hostSession, this, aEventTarget);
   }
 
   // take the protocol instance and add it to the connectionCache
   if (protocolInstance)
     m_connectionCache.AppendObject(protocolInstance);
   *aImapConnection = protocolInstance; // this is already ref counted.
   return rv;
 }
--- a/mailnews/imap/src/nsImapMailFolder.cpp
+++ b/mailnews/imap/src/nsImapMailFolder.cpp
@@ -128,16 +128,17 @@
 #include "nsArrayEnumerator.h"
 #include "nsAutoSyncManager.h"
 #include "nsIMsgFilterCustomAction.h"
 #include "nsMsgReadStateTxn.h"
 #include "nsIStringEnumerator.h"
 #include "nsIMsgStatusFeedback.h"
 #include "nsAlgorithm.h"
 #include "nsPrintfCString.h"
+#include "nsMsgLineBuffer.h"
 
 static NS_DEFINE_CID(kRDFServiceCID, NS_RDFSERVICE_CID);
 static NS_DEFINE_CID(kParseMailMsgStateCID, NS_PARSEMAILMSGSTATE_CID);
 static NS_DEFINE_CID(kCImapHostSessionList, NS_IIMAPHOSTSESSIONLIST_CID);
 
 nsIAtom* nsImapMailFolder::mImapHdrDownloadedAtom = nsnull;
 
 extern PRLogModuleInfo *gAutoSyncLog;
@@ -2835,33 +2836,29 @@ NS_IMETHODIMP nsImapMailFolder::UpdateIm
     aSpec->GetBox_flags(&boxFlags);
     FindKeysToDelete(existingKeys, keysToDelete, flagState, boxFlags);
     // if this is the result of an expunge then don't grab headers
     if (!(boxFlags & kJustExpunged))
       FindKeysToAdd(existingKeys, keysToFetch, numNewUnread, flagState);
   }
   if (!keysToDelete.IsEmpty() && mDatabase)
   {
-    PRUint32 total;
-
     nsCOMPtr<nsIMutableArray> hdrsToDelete(do_CreateInstance(NS_ARRAY_CONTRACTID));
     MsgGetHeadersFromKeys(mDatabase, keysToDelete, hdrsToDelete);
     // Notify nsIMsgFolderListeners of a mass delete, but only if we actually have headers
     PRUint32 numHdrs;
     hdrsToDelete->GetLength(&numHdrs);
     if (numHdrs)
     {
       nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID));
       if (notifier)
         notifier->NotifyMsgsDeleted(hdrsToDelete);
     }
-
     // It would be nice to notify RDF or whoever of a mass delete here.
     mDatabase->DeleteMessages(keysToDelete.Length(), keysToDelete.Elements(), nsnull);
-    total = keysToDelete.Length();
   }
   PRInt32 numUnreadFromServer;
   aSpec->GetNumUnseenMessages(&numUnreadFromServer);
   
   PRBool partialUIDFetch;
   flagState->GetPartialUIDFetch(&partialUIDFetch);
   
   // For partial UID fetches, we can only trust the numUnread from the server.
@@ -3002,17 +2999,17 @@ NS_IMETHODIMP nsImapMailFolder::ParseMsg
       inputStream->ShareData(msgHdrs, strlen(msgHdrs));
       GetMessageHeader(msgKey, getter_AddRefs(msgHdr));
       if (msgHdr)
         GetMsgPreviewTextFromStream(msgHdr, inputStream);
       continue;
     }
     if (mDatabase && NS_SUCCEEDED(mDatabase->ContainsKey(msgKey, &containsKey)) && containsKey)
     {
-      NS_ASSERTION(PR_FALSE, "downloading hdrs for hdr we already have");
+      NS_ERROR("downloading hdrs for hdr we already have");
       continue;
     }
     nsresult rv = SetupHeaderParseStream(msgSize, EmptyCString(), nsnull);
     NS_ENSURE_SUCCESS(rv, rv);
     headerInfo->GetMsgHdrs(&msgHdrs);
     rv = ParseAdoptedHeaderLine(msgHdrs, msgKey);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = NormalEndHeaderParseStream(aProtocol, aImapUrl);
@@ -3182,18 +3179,33 @@ nsresult nsImapMailFolder::NormalEndHead
           NotifyFolderEvent(mFiltersAppliedAtom);
         }
       }
     }
   }
   // here we need to tweak flags from uid state..
   if (mDatabase && (!m_msgMovedByFilter || ShowDeletedMessages()))
   {
+    nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID));
+    // Check if this header corresponds to a pseudo header
+    // we have from doing a pseudo-offline move and then downloading
+    // the real header from the server. In that case, we notify
+    // db/folder listeners that the pseudo-header has become the new
+    // header, i.e., the key has changed.
+    nsCString newMessageId;
+    nsMsgKey pseudoKey = nsMsgKey_None;
+    newMsgHdr->GetMessageId(getter_Copies(newMessageId));
+    if (m_pseudoHdrs.IsInitialized())
+      m_pseudoHdrs.Get(newMessageId, &pseudoKey);
+    if (notifier && pseudoKey != nsMsgKey_None)
+    {
+      notifier->NotifyMsgKeyChanged(pseudoKey, newMsgHdr);
+      m_pseudoHdrs.Remove(newMessageId);
+    }
     mDatabase->AddNewHdrToDB(newMsgHdr, PR_TRUE);
-    nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID));
     if (notifier)
       notifier->NotifyMsgAdded(newMsgHdr);
     // mark the header as not yet reported classified
     OrProcessingFlags(m_curMsgUid, nsMsgProcessingFlags::NotReportedClassified);
   }
   // adjust highestRecordedUID
   if (mDatabase)
   {
@@ -3891,16 +3903,31 @@ nsImapMailFolder::ReplayOfflineMoveCopy(
     NS_ENSURE_SUCCESS(rv, rv);
     nsCOMPtr <nsIUrlListener> folderListener = do_QueryInterface(aDstFolder);
     if (folderListener)
       mailnewsUrl->RegisterListener(folderListener);
   }
   return rv;
 }
 
+NS_IMETHODIMP nsImapMailFolder::AddMoveResultPseudoKey(nsMsgKey aMsgKey)
+{
+  nsCOMPtr<nsIMsgDBHdr> pseudoHdr;
+  mDatabase->GetMsgHdrForKey(aMsgKey, getter_AddRefs(pseudoHdr));
+  nsCString messageId;
+  pseudoHdr->GetMessageId(getter_Copies(messageId));
+  // err on the side of caution and ignore messages w/o messageid.
+  if (messageId.IsEmpty())
+    return NS_OK;
+  if (!m_pseudoHdrs.IsInitialized())
+    m_pseudoHdrs.Init(10);
+  m_pseudoHdrs.Put(messageId, aMsgKey);
+  return NS_OK;
+}
+
 NS_IMETHODIMP nsImapMailFolder::StoreImapFlags(PRInt32 flags, PRBool addFlags,
                                                nsMsgKey *keys, PRUint32 numKeys,
                                                nsIUrlListener *aUrlListener)
 {
   nsresult rv;
   if (!WeAreOffline())
   {
     nsCOMPtr<nsIImapService> imapService = do_GetService(NS_IMAPSERVICE_CONTRACTID, &rv);
@@ -5701,18 +5728,16 @@ nsImapMailFolder::GetMessageId(nsIImapUr
   }
   return rv;
 }
 
 NS_IMETHODIMP
 nsImapMailFolder::HeaderFetchCompleted(nsIImapProtocol* aProtocol)
 {
   nsCOMPtr <nsIMsgWindow> msgWindow; // we might need this for the filter plugins.
-  if (mDatabase)
-    mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
   if (mBackupDatabase)
     RemoveBackupMsgDatabase();
 
   SetSizeOnDisk(mFolderSize);
   PRInt32 numNewBiffMsgs = 0;
   if (m_performingBiff)
     GetNumNewMessages(PR_FALSE, &numNewBiffMsgs);
 
@@ -6694,18 +6719,16 @@ nsImapMailFolder::CopyNextStreamMessage(
     nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID));
     if (notifier)
     {
       PRUint32 numHdrs;
       mailCopyState->m_messages->GetLength(&numHdrs);
       if (numHdrs)
         notifier->NotifyMsgsMoveCopyCompleted(mailCopyState->m_isMove, mailCopyState->m_messages, this, nsnull);
     }
-
-
     if (mailCopyState->m_isMove)
     {
       nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(mailCopyState->m_srcSupport, &rv));
       if (NS_SUCCEEDED(rv) && srcFolder)
       {
         srcFolder->DeleteMessages(mailCopyState->m_messages, nsnull,
           PR_TRUE, PR_TRUE, nsnull, PR_FALSE);
         // we want to send this notification after the source messages have
@@ -6965,16 +6988,20 @@ nsresult nsImapMailFolder::CopyMessagesO
   srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(srcDbFolderInfo), getter_AddRefs(sourceMailDB));
   PRBool deleteToTrash = PR_FALSE;
   PRBool deleteImmediately = PR_FALSE;
   PRUint32 srcCount;
   messages->GetLength(&srcCount);
   nsCOMPtr<nsIImapIncomingServer> imapServer;
   rv = GetImapIncomingServer(getter_AddRefs(imapServer));
   nsCOMPtr<nsIMutableArray> msgHdrsCopied(do_CreateInstance(NS_ARRAY_CONTRACTID));
+  nsCOMPtr<nsIMutableArray> destMsgHdrs(do_CreateInstance(NS_ARRAY_CONTRACTID));
+
+  if (!msgHdrsCopied || !destMsgHdrs)
+    return NS_ERROR_OUT_OF_MEMORY;
 
   if (NS_SUCCEEDED(rv) && imapServer)
   {
     nsMsgImapDeleteModel deleteModel;
     imapServer->GetDeleteModel(&deleteModel);
     deleteToTrash = (deleteModel == nsMsgImapDeleteModels::MoveToTrash);
     deleteImmediately = (deleteModel == nsMsgImapDeleteModels::DeleteNoTrash);
   }
@@ -7140,17 +7167,21 @@ nsresult nsImapMailFolder::CopyMessagesO
           {
             NS_ASSERTION(PR_FALSE, "failed to copy hdr");
             stopit = rv;
           }
 
           if (NS_SUCCEEDED(stopit))
           {
             PRBool hasMsgOffline = PR_FALSE;
+
+            destMsgHdrs->AppendElement(newMailHdr, PR_FALSE);
             srcFolder->HasMsgOffline(originalKey, &hasMsgOffline);
+            newMailHdr->SetUint32Property("pseudoHdr", 1);
+
             if (inputStream && hasMsgOffline && !isLocked)
               CopyOfflineMsgBody(srcFolder, newMailHdr, mailHdr, inputStream,
                                  outputStream);
             else
               mDatabase->MarkOffline(fakeBase + sourceKeyIndex, PR_FALSE, nsnull);
 
             nsCOMPtr <nsIMsgOfflineImapOperation> destOp;
             mDatabase->GetOfflineOpForKey(fakeBase + sourceKeyIndex, PR_TRUE, getter_AddRefs(destOp));
@@ -7232,17 +7263,17 @@ nsresult nsImapMailFolder::CopyMessagesO
 
   // Do this before delete, as it destroys the messages
   PRUint32 numHdrs;
   msgHdrsCopied->GetLength(&numHdrs);
   if (numHdrs)
   {
     nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID));
     if (notifier)
-      notifier->NotifyMsgsMoveCopyCompleted(isMove, msgHdrsCopied, this, nsnull);
+      notifier->NotifyMsgsMoveCopyCompleted(isMove, msgHdrsCopied, this, destMsgHdrs);
   }
 
   if (isMove && (deleteToTrash || deleteImmediately))
     sourceMailDB->DeleteMessages(keysToDelete.Length(), keysToDelete.Elements(), nsnull);
 
   nsCOMPtr<nsISupports> srcSupport = do_QueryInterface(srcFolder);
   OnCopyCompleted(srcSupport, rv);
 
@@ -8117,18 +8148,123 @@ nsImapMailFolder::InitCopyState(nsISuppo
   m_copyState->m_selectedState = selectedState;
   m_copyState->m_msgWindow = msgWindow;
   if (listener)
     m_copyState->m_listener = do_QueryInterface(listener, &rv);
   return rv;
 }
 
 nsresult
+nsImapMailFolder::CopyFileToOfflineStore(nsILocalFile *srcFile)
+{
+  nsCOMPtr<nsIMsgDatabase> db;
+  nsresult rv = GetMsgDatabase(getter_AddRefs(db));
+
+  if (mDatabase)
+  {
+    nsMsgKey fakeKey;
+    mDatabase->GetNextFakeOfflineMsgKey(&fakeKey);
+    nsCOMPtr<nsIMutableArray> messages(do_CreateInstance(NS_ARRAY_CONTRACTID));
+
+    nsCOMPtr<nsIMsgOfflineImapOperation> op;
+    rv = mDatabase->GetOfflineOpForKey(fakeKey, PR_TRUE, getter_AddRefs(op));
+    if (NS_SUCCEEDED(rv) && op)
+    {
+      nsCString destFolderUri;
+      GetURI(destFolderUri);
+      op->SetOperation(nsIMsgOfflineImapOperation::kMoveResult);
+      op->SetDestinationFolderURI(destFolderUri.get());
+      nsCOMPtr<nsIOutputStream> offlineStore;
+      rv = GetOfflineStoreOutputStream(getter_AddRefs(offlineStore));
+      SetFlag(nsMsgFolderFlags::OfflineEvents);
+
+      if (NS_SUCCEEDED(rv) && offlineStore)
+      {
+        PRInt64 curOfflineStorePos = 0;
+        nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(offlineStore);
+        if (seekable)
+          seekable->Tell(&curOfflineStorePos);
+        else
+        {
+          NS_ERROR("needs to be a random store!");
+          return NS_ERROR_FAILURE;
+        }
+
+        nsCOMPtr<nsIInputStream> inputStream;
+        nsCOMPtr<nsIMsgParseMailMsgState> msgParser =
+          do_CreateInstance(NS_PARSEMAILMSGSTATE_CONTRACTID, &rv);
+        msgParser->SetMailDB(mDatabase);
+
+        rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), srcFile);
+        if (NS_SUCCEEDED(rv) && inputStream)
+        {
+          // now, copy the temp file to the offline store for the cur folder.
+          PRInt32 inputBufferSize = 10240;
+          nsMsgLineStreamBuffer *inputStreamBuffer =
+            new nsMsgLineStreamBuffer(inputBufferSize, PR_TRUE, PR_FALSE);
+          PRInt64 fileSize;
+          srcFile->GetFileSize(&fileSize);
+          PRUint32 bytesWritten;
+          rv = NS_OK;
+          msgParser->SetState(nsIMsgParseMailMsgState::ParseHeadersState);
+          // set the env pos to fake key so the msg hdr will have that for a key
+          msgParser->SetEnvelopePos(fakeKey);
+          PRBool needMoreData = PR_FALSE;
+          char * newLine = nsnull;
+          PRUint32 numBytesInLine = 0;
+          do
+          {
+            newLine = inputStreamBuffer->ReadNextLine(inputStream, numBytesInLine, needMoreData);
+            if (newLine)
+            {
+              msgParser->ParseAFolderLine(newLine, numBytesInLine);
+              rv = offlineStore->Write(newLine, numBytesInLine, &bytesWritten);
+              NS_Free(newLine);
+            }
+          } while (newLine);
+
+          nsCOMPtr<nsIMsgDBHdr> fakeHdr;
+          msgParser->FinishHeader();
+          msgParser->GetNewMsgHdr(getter_AddRefs(fakeHdr));
+          if (fakeHdr)
+          {
+            if (NS_SUCCEEDED(rv) && fakeHdr)
+            {
+              PRUint32 resultFlags;
+              fakeHdr->SetMessageOffset(curOfflineStorePos);
+              fakeHdr->OrFlags(nsMsgMessageFlags::Offline | nsMsgMessageFlags::Read, &resultFlags);
+              fakeHdr->SetOfflineMessageSize(fileSize);
+              fakeHdr->SetUint32Property("pseudoHdr", 1);
+              mDatabase->AddNewHdrToDB(fakeHdr, PR_TRUE /* notify */);
+              SetFlag(nsMsgFolderFlags::OfflineEvents);
+              messages->AppendElement(fakeHdr, PR_FALSE);
+              SetPendingAttributes(messages, PR_FALSE);
+            }
+          }
+          inputStream->Close();
+          inputStream = nsnull;
+          delete inputStreamBuffer;
+        }
+      }
+    }
+  }
+  return rv;
+}
+
+nsresult
 nsImapMailFolder::OnCopyCompleted(nsISupports *srcSupport, nsresult rv)
 {
+  // if it's a file, and the copy succeeded, then fcc the offline
+  // store, and add a kMoveResult offline op.
+  if (NS_SUCCEEDED(rv) && m_copyState)
+  {
+    nsCOMPtr<nsILocalFile> srcFile(do_QueryInterface(srcSupport));
+    if (srcFile && (mFlags & nsMsgFolderFlags::Offline) && !WeAreOffline())
+      (void) CopyFileToOfflineStore(srcFile);
+  }
   m_copyState = nsnull;
   nsresult result;
   nsCOMPtr<nsIMsgCopyService> copyService = do_GetService(NS_MSGCOPYSERVICE_CONTRACTID, &result);
   NS_ENSURE_SUCCESS(result, result);
   return copyService->NotifyCompletion(srcSupport, this, rv);
 }
 
 nsresult nsImapMailFolder::CreateBaseMessageURI(const nsACString& aURI)
--- a/mailnews/imap/src/nsImapMailFolder.h
+++ b/mailnews/imap/src/nsImapMailFolder.h
@@ -371,16 +371,17 @@ protected:
                              PRUint16 userFlags, nsCString& keywords);
   nsresult NotifyMessageFlagsFromHdr(nsIMsgDBHdr *dbHdr, nsMsgKey msgKey, PRUint32 flags);
 
   nsresult SetupHeaderParseStream(PRUint32 size, const nsACString& content_type, nsIMailboxSpec *boxSpec);
   nsresult  ParseAdoptedHeaderLine(const char *messageLine, PRUint32 msgKey);
   nsresult  NormalEndHeaderParseStream(nsIImapProtocol *aProtocol, nsIImapUrl *imapUrl);
 
   void EndOfflineDownload();
+  nsresult CopyFileToOfflineStore(nsILocalFile *srcFile);
 
   nsresult MarkMessagesImapDeleted(nsTArray<nsMsgKey> *keyArray, PRBool deleted, nsIMsgDatabase *db);
 
   // Notifies imap autosync that it should update this folder when it
   // gets a chance.
   void NotifyHasPendingMsgs();
   void UpdatePendingCounts();
   void SetIMAPDeletedFlag(nsIMsgDatabase *mailDB, const nsTArray<nsMsgKey> &msgids, PRBool markDeleted);
@@ -517,24 +518,26 @@ protected:
   PRUint32     m_aclFlags;
   PRUint32     m_supportedUserFlags;
 
   static nsIAtom* mImapHdrDownloadedAtom;
 
   // offline imap support
   PRBool m_downloadingFolderForOfflineUse;
   PRBool m_filterListRequiresBody;
-  
+
   // auto-sync (preemptive download) support
   nsRefPtr<nsAutoSyncState> m_autoSyncStateObj;
-  
+
   // Quota support
   nsCString m_folderQuotaRoot;
   PRUint32 m_folderQuotaUsedKB;
   PRUint32 m_folderQuotaMaxKB;
-  
+
   // Pseudo-Offline Playback support
   nsPlaybackRequest *m_pendingPlaybackReq;
   nsCOMPtr<nsITimer> m_playbackTimer;
   nsTArray<nsRefPtr<nsImapMoveCopyMsgTxn> > m_pendingOfflineMoves;
-
+  // hash table of mapping between messageids and message keys
+  // for pseudo hdrs.
+  nsDataHashtable<nsCStringHashKey, nsMsgKey> m_pseudoHdrs;
 };
 #endif
--- a/mailnews/imap/src/nsImapOfflineSync.cpp
+++ b/mailnews/imap/src/nsImapOfflineSync.cpp
@@ -608,39 +608,43 @@ PRBool nsImapOfflineSync::DestFolderOnSa
 
   PRBool sameServer = PR_FALSE;
   if (NS_SUCCEEDED(m_currentFolder->GetServer(getter_AddRefs(srcServer))) 
     && NS_SUCCEEDED(destFolder->GetServer(getter_AddRefs(dstServer))))
     dstServer->Equals(srcServer, &sameServer);
   return sameServer;
 }
 
-void nsImapOfflineSync::ProcessCopyOperation(nsIMsgOfflineImapOperation *currentOp)
+void nsImapOfflineSync::ProcessCopyOperation(nsIMsgOfflineImapOperation *aCurrentOp)
 {
+  nsCOMPtr<nsIMsgOfflineImapOperation> currentOp = aCurrentOp;
+
   nsTArray<nsMsgKey> matchingFlagKeys;
   PRUint32 currentKeyIndex = m_KeyIndex;
   nsCString copyDestination;
   currentOp->GetCopyDestination(0, getter_Copies(copyDestination));
   PRBool copyMatches = PR_TRUE;
-  
+  nsresult rv;
+
   do { // loop for all messsages with the same destination
     if (copyMatches)
     {
       nsMsgKey curKey;
       currentOp->GetMessageKey(&curKey);
       matchingFlagKeys.AppendElement(curKey);
       currentOp->SetPlayingBack(PR_TRUE);
       m_currentOpsToClear.AppendObject(currentOp);
     }
     currentOp = nsnull;
 
     if (++currentKeyIndex < m_CurrentKeys.Length())
     {
       nsCString nextDestination;
-      nsresult rv = m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex], PR_FALSE, &currentOp);
+      rv = m_currentDB->GetOfflineOpForKey(m_CurrentKeys[currentKeyIndex],
+                                           PR_FALSE, getter_AddRefs(currentOp));
       copyMatches = PR_FALSE;
       if (NS_SUCCEEDED(rv) && currentOp)
       {
         nsOfflineImapOperationType opType; 
         currentOp->GetOperation(&opType);
         if (opType & nsIMsgOfflineImapOperation::kMsgCopy)
         {
           currentOp->GetCopyDestination(0, getter_Copies(nextDestination));
@@ -658,36 +662,33 @@ void nsImapOfflineSync::ProcessCopyOpera
   // going to fail, so clear them out and move on.
   if (!destFolder)
   {
     NS_ERROR("trying to playing back copy to non-existent folder");
     ClearCurrentOps();
     ProcessNextOperation();
     return;
   }
-  nsresult rv;
   nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_currentFolder);
   if (imapFolder && DestFolderOnSameServer(destFolder))
   {
     rv = imapFolder->ReplayOfflineMoveCopy(matchingFlagKeys.Elements(), matchingFlagKeys.Length(), PR_FALSE, destFolder,
                    this, m_window);
   }
   else
   {
     nsCOMPtr<nsIMutableArray> messages(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv));
     if (messages && NS_SUCCEEDED(rv))
     {
       for (PRUint32 keyIndex = 0; keyIndex < matchingFlagKeys.Length(); keyIndex++)
       {
         nsCOMPtr<nsIMsgDBHdr> mailHdr = nsnull;
         rv = m_currentFolder->GetMessageHeader(matchingFlagKeys.ElementAt(keyIndex), getter_AddRefs(mailHdr));
         if (NS_SUCCEEDED(rv) && mailHdr)
-        {
           messages->AppendElement(mailHdr, PR_FALSE);
-        }
       }
       nsCOMPtr<nsIMsgCopyService> copyService = do_GetService(NS_MSGCOPYSERVICE_CONTRACTID, &rv);
       if (copyService)
         copyService->CopyMessages(m_currentFolder, messages, destFolder, PR_FALSE, this, m_window, PR_FALSE);
     }
   }
 }
 
@@ -823,26 +824,33 @@ nsresult nsImapOfflineSync::ProcessNextO
             
             if (opType == nsIMsgOfflineImapOperation::kMoveResult)
             {
               nsMsgKey curKey;
               currentOp->GetMessageKey(&curKey);
               m_currentDB->RemoveOfflineOp(currentOp);
               deletedGhostMsgs = PR_TRUE;
 
+              // Remember the pseudo headers before we delete them,
+              // and when we download new headers, tell listeners about the
+              // message key change between the pseudo headers and the real
+              // downloaded headers. Note that we're not currently sending
+              // a msgsDeleted notifcation for these headers, but the
+              // db listeners are notified about the deletion.
               // for imap folders, we should adjust the pending counts, because we
               // have a header that we know about, but don't have in the db.
               nsCOMPtr <nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_currentFolder);
               if (imapFolder)
               {
                 PRBool hdrIsRead;
                 m_currentDB->IsRead(curKey, &hdrIsRead);
                 imapFolder->ChangePendingTotal(1);
                 if (!hdrIsRead)
                   imapFolder->ChangePendingUnread(1);
+                imapFolder->AddMoveResultPseudoKey(curKey);
               }
               m_currentDB->DeleteMessage(curKey, nsnull, PR_FALSE);
             }
           }
         }
         
         if (deletedGhostMsgs)
           m_currentFolder->SummaryChanged();
--- a/mailnews/imap/test/unit/test_imapAttachmentSaves.js
+++ b/mailnews/imap/test/unit/test_imapAttachmentSaves.js
@@ -138,18 +138,18 @@ function testDetach()
   // directory.
   let checkFile = gProfileDir.clone();
   checkFile.append(kAttachFileName);
   do_check_true(checkFile.exists());
 
   // The message should now have a detached attachment. Read the message,
   //  and search for "AttachmentDetached" which is added on detachment.
 
-  // Get the message header
-  let msgHdr = firstMsgHdr(gIMAPInbox);
+  // Get the message header - detached copy has UID 2.
+  let msgHdr = gIMAPInbox.GetMessageHeader(2);
   do_check_neq(msgHdr, null);
   let messageContent = getContentFromMessage(msgHdr);
   do_check_true(messageContent.indexOf("AttachmentDetached") != -1);
 }
 
 // Cleanup
 function endTest()
 {
--- a/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js
+++ b/mailnews/imap/test/unit/test_nsIMsgFolderListenerIMAP.js
@@ -28,16 +28,19 @@ const gMsgFile4 = do_get_file("../../../
 const gMsgFile5 = do_get_file("../../../data/bugmail6");
 
 // Copied straight from the example files
 const gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
 const gMsgId2 = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
 const gMsgId3 = "4849BF7B.2030800@example.com";
 const gMsgId4 = "bugmail7.m47LtAEf007542@mrapp51.mozilla.org";
 const gMsgId5 = "bugmail6.m47LtAEf007542@mrapp51.mozilla.org";
+var gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"]
+                  .createInstance(Components.interfaces.nsIMsgWindow);
+
 
 function addFolder(parent, folderName, storeIn)
 {
   gExpectedEvents = [[gMFNService.folderAdded, parent, folderName, storeIn]];
   // No copy listener notification for this
   gCurrStatus |= kStatus.onStopCopyDone;
   parent.createSubfolder(folderName, null);
   gCurrStatus |= kStatus.functionCallDone;
@@ -121,30 +124,32 @@ function addMessagesToServer(messages, m
 
 function copyMessages(messages, isMove, srcFolder, destFolder)
 {
   let array = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
   messages.forEach(function (message)
   {
     array.appendElement(message, false);
   });
-  gExpectedEvents = [[gMFNService.msgsMoveCopyCompleted, isMove, messages, destFolder]];
+  gExpectedEvents = [[gMFNService.msgsMoveCopyCompleted, isMove, messages, destFolder, true]];
   // We'll also get the msgAdded events when we go and update the destination
   // folder
   messages.forEach(function (message)
   {
     // We can't use the headers directly, because the notifications we'll
     // receive are for message headers in the destination folder
+    gExpectedEvents.push([gMFNService.msgKeyChanged,
+                          {expectedMessageId: message.messageId}]);
     gExpectedEvents.push([gMFNService.msgAdded,
                           {expectedMessageId: message.messageId}]);
   });
   gExpectedEvents.push([gMFNService.msgsClassified,
                         [hdr.messageId for each (hdr in messages)],
                         false, false]);
-  gCopyService.CopyMessages(srcFolder, array, destFolder, isMove, copyListener, null, true);
+  gCopyService.CopyMessages(srcFolder, array, destFolder, isMove, copyListener, gMsgWindow, true);
   gCurrStatus |= kStatus.functionCallDone;
 
   gServer.performTest("COPY");
 
   gFolderBeingUpdated = destFolder;
   doUpdateFolder(gTest);
   if (gCurrStatus == kStatus.everythingDone)
     resetStatusAndProceed();
@@ -179,17 +184,17 @@ const gTestArray =
   // Add another couple of messages, this time to another folder on the server
   function testNewMessageArrival2() {
     addMessagesToServer([{file: gMsgFile4, messageId: gMsgId4},
                          {file: gMsgFile5, messageId: gMsgId5}],
                         gIMAPDaemon.getMailbox("INBOX"), gIMAPInbox);
   },
 
   // Moving/copying messages (this doesn't work right now)
-  /* function testCopyMessages1() { copyMessages([gMsgHdrs[0].hdr, gMsgHdrs[1].hdr], false, gIMAPInbox, gIMAPFolder3) } */
+  function testCopyMessages1() { copyMessages([gMsgHdrs[0].hdr, gMsgHdrs[1].hdr], false, gIMAPInbox, gIMAPFolder3) }
 ];
 
 function run_test()
 {
   // This is before any of the actual tests, so...
   gTest = 0;
 
   // Add a listener.
@@ -265,16 +270,17 @@ function doTest(test)
     testFn();
   }
   else
   {
     gMFNService.removeListener(gMFListener);
     // Cleanup, null out everything, close all cached connections and stop the
     // server
     gRootFolder = null;
+    gIMAPInbox.msgDatabase = null;
     gIMAPInbox = null;
     gIMAPFolder2 = null;
     gIMAPFolder3 = null;
     gIMAPTrashFolder = null;
     do_timeout(1000, endTest);
   }
 }
 
--- a/mailnews/local/src/nsRssIncomingServer.cpp
+++ b/mailnews/local/src/nsRssIncomingServer.cpp
@@ -277,16 +277,22 @@ NS_IMETHODIMP nsRssIncomingServer::MsgsD
 
 NS_IMETHODIMP nsRssIncomingServer::MsgsMoveCopyCompleted(
   PRBool aMove, nsIArray *aSrcMsgs, nsIMsgFolder *aDestFolder,
   nsIArray *aDestMsgs)
 {
   return NS_ERROR_NOT_IMPLEMENTED;
 }
 
+NS_IMETHODIMP nsRssIncomingServer::MsgKeyChanged(nsMsgKey aOldKey,
+                                                 nsIMsgDBHdr *aNewHdr)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 NS_IMETHODIMP nsRssIncomingServer::FolderAdded(nsIMsgFolder *aFolder)
 {
   return FolderChanged(aFolder, PR_FALSE);
 }
 
 NS_IMETHODIMP nsRssIncomingServer::FolderDeleted(nsIMsgFolder *aFolder)
 {
   return FolderChanged(aFolder, PR_TRUE);
--- a/mailnews/test/resources/msgFolderListenerSetup.js
+++ b/mailnews/test/resources/msgFolderListenerSetup.js
@@ -1,40 +1,21 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* ***** BEGIN LICENSE BLOCK *****
- *
- * Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/licenses/publicdomain/
- *
- * ***** END LICENSE BLOCK ***** */
-
-/*
- * Setup for nsIMsgFolderListener tests.
- *
- * To create a test on top of this,
- *
- * - define a doTest function which accepts the number of the test as an argument.
- * - for each test, store the expected events as an array in gExpectedEvents, with
- *   format [event, item array] or [event, is move, item array, destination folder].
- * - use the copyListener and gMFListener
- * - make sure you set the function call flag once your function call is done.
- */
-
 const nsIMsgDBHdr = Ci.nsIMsgDBHdr;
 const nsIArray = Ci.nsIArray;
 const nsIMsgFolder = Ci.nsIMsgFolder;
 
 const gMFNService = Cc["@mozilla.org/messenger/msgnotificationservice;1"]
                       .getService(Ci.nsIMsgFolderNotificationService);
 
 const allTestedEvents =
   gMFNService.msgAdded |
   gMFNService.msgsClassified |
   gMFNService.msgsDeleted |
   gMFNService.msgsMoveCopyCompleted |
+  gMFNService.msgKeyChanged |
   gMFNService.folderAdded |
   gMFNService.folderDeleted |
   gMFNService.folderMoveCopyCompleted |
   gMFNService.folderRenamed |
   gMFNService.itemEvent;
 
 const gCopyService = Cc["@mozilla.org/messenger/messagecopyservice;1"]
                       .getService(Ci.nsIMsgCopyService);
@@ -118,16 +99,27 @@ var gMFListener =
     if (gExpectedEvents.length == 0)
     {
       gCurrStatus |= kStatus.notificationsDone;
       if (gCurrStatus == kStatus.everythingDone)
         resetStatusAndProceed();
     }
   },
 
+  msgKeyChanged: function(aOldKey, aNewMsgHdr)
+  {
+    verify([gMFNService.msgKeyChanged, aOldKey, aNewMsgHdr]);
+    if (gExpectedEvents.length == 0)
+    {
+      gCurrStatus |= kStatus.notificationsDone;
+      if (gCurrStatus == kStatus.everythingDone)
+        resetStatusAndProceed();
+    }
+  },
+  
   folderAdded: function(aFolder)
   {
     verify([gMFNService.folderAdded, aFolder]);
     if (gExpectedEvents.length == 0)
     {
       gCurrStatus |= kStatus.notificationsDone;
       if (gCurrStatus == kStatus.everythingDone)
         resetStatusAndProceed();
@@ -312,47 +304,42 @@ function verify(event)
     else { // actual headers
       hasExactlyElements(expected[1], event[1]);
     }
     // aJunkProcessed: was the message processed for junk?
     do_check_eq(expected[2], event[2]);
     // aTraitProcessed: was the message processed for traits?
     do_check_eq(expected[3], event[3]);
     break;
+  case gMFNService.msgKeyChanged:
+    do_check_eq(expected[1].messageId, event[2].expectedMessageId);
+    break;
   case gMFNService.msgsMoveCopyCompleted:
   case gMFNService.folderMoveCopyCompleted:
     // Check: Move or copy as expected.
     do_check_eq(expected[1], event[1]);
 
     // Check: headers match/folder matches.
     hasExactlyElements(expected[2], event[2]);
 
     // Check: destination folder matches.
     do_check_eq(expected[3], event[3]);
 
     if (eventType == gMFNService.folderMoveCopyCompleted)
       break;
 
-    // Check: destination headers.  We only expect these in the local folder
-    //  case, and in that case, we will not have heard about the headers ahead
-    //  of time, so the best we can do is make sure they match up.  To this end,
-    //  if null is expected then we check for null.  If true is expected, then
+    // Check: destination headers.  We expect these for local and imap folders,
+    //  but we will not have heard about the headers ahead of time,
+    //  so the best we can do is make sure they match up.  To this end,
     //  we check that the message-id header values match up.
-    if (expected[4] == null)
+    for (let iMsg = 0; iMsg < event[2].length; iMsg++)
     {
-      do_check_eq(null, event[4]);
-    }
-    else
-    {
-      for (let iMsg = 0; iMsg < event[2].length; iMsg++)
-      {
-        let srcHdr = event[2].queryElementAt(iMsg, nsIMsgDBHdr);
-        let destHdr = event[4].queryElementAt(iMsg, nsIMsgDBHdr);
-        do_check_eq(srcHdr.messageId, destHdr.messageId);
-      }
+      let srcHdr = event[2].queryElementAt(iMsg, nsIMsgDBHdr);
+      let destHdr = event[4].queryElementAt(iMsg, nsIMsgDBHdr);
+      do_check_eq(srcHdr.messageId, destHdr.messageId);
     }
     break;
   case gMFNService.folderAdded:
     // Check: parent folder matches
     do_check_eq(event[1].parent, expected[1]);
 
     // Check: folder name matches
     do_check_eq(event[1].prettyName, expected[2]);