fix various causes of corruption of offline imap store, r=standard8, sr=neil, 468595, plus limited unit test
authorDavid Bienvenu <bienvenu@nventure.com>
Sat, 29 Aug 2009 12:14:45 -0700
changeset 3450 8f4aadd44e102040752f31290d21491b62b187e1
parent 3449 880ac704c5b75ddc9a19010239998daa1f16a027
child 3451 f3cf62cbdf2e916a7a317ef087b60f16eec4923e
push idunknown
push userunknown
push dateunknown
reviewersstandard8, neil, 468595, plus
bugs468595
fix various causes of corruption of offline imap store, r=standard8, sr=neil, 468595, plus limited unit test
mailnews/base/util/nsMsgDBFolder.cpp
mailnews/base/util/nsMsgDBFolder.h
mailnews/imap/public/nsIImapMessageSink.idl
mailnews/imap/public/nsIImapUrl.idl
mailnews/imap/src/nsImapMailFolder.cpp
mailnews/imap/src/nsImapMailFolder.h
mailnews/imap/src/nsImapProtocol.cpp
mailnews/imap/src/nsImapService.cpp
mailnews/imap/src/nsImapUrl.cpp
mailnews/imap/src/nsImapUrl.h
mailnews/imap/test/unit/test_imapStoreMsgOffline.js
mailnews/test/data/external-attach-test
mailnews/test/data/image-attach-test
--- a/mailnews/base/util/nsMsgDBFolder.cpp
+++ b/mailnews/base/util/nsMsgDBFolder.cpp
@@ -1575,16 +1575,17 @@ nsresult nsMsgDBFolder::WriteStartOfNewL
   char *ct;
   PRUint32 writeCount;
   time_t now = time ((time_t*) 0);
   ct = ctime(&now);
   ct[24] = 0;
   result = "From - ";
   result += ct;
   result += MSG_LINEBREAK;
+  m_bytesAddedToLocalMsg = result.Length();
 
   nsCOMPtr <nsISeekableStream> seekable;
   nsInt64 curStorePos;
 
   if (m_offlineHeader)
     seekable = do_QueryInterface(m_tempMessageStream);
 
   if (seekable)
@@ -1601,24 +1602,24 @@ nsresult nsMsgDBFolder::WriteStartOfNewL
   {
     PRInt64 tellPos;
     seekable->Seek(PR_SEEK_CUR, 0); // seeking causes a flush, w/o syncing
     seekable->Tell(&tellPos);
     curStorePos = tellPos;
     m_offlineHeader->SetStatusOffset((PRUint32) curStorePos);
   }
 
-  result = "X-Mozilla-Status: 0001";
-  result += MSG_LINEBREAK;
-  m_tempMessageStream->Write(result.get(), result.Length(),
+  NS_NAMED_LITERAL_CSTRING(MozillaStatus, "X-Mozilla-Status: 0001" MSG_LINEBREAK);
+  m_tempMessageStream->Write(MozillaStatus.get(), MozillaStatus.Length(),
                              &writeCount);
-  result =  "X-Mozilla-Status2: 00000000";
-  result += MSG_LINEBREAK;
-  return m_tempMessageStream->Write(result.get(), result.Length(),
-                             &writeCount);
+  m_bytesAddedToLocalMsg += writeCount;
+  NS_NAMED_LITERAL_CSTRING(MozillaStatus2, "X-Mozilla-Status2: 00000000" MSG_LINEBREAK);
+  m_bytesAddedToLocalMsg += MozillaStatus2.Length();
+  return m_tempMessageStream->Write(MozillaStatus2.get(),
+                                    MozillaStatus2.Length(), &writeCount);
 }
 
 nsresult nsMsgDBFolder::StartNewOfflineMessage()
 {
   nsresult rv = NS_OK;
   if (!m_tempMessageStream)
     rv = GetOfflineStoreOutputStream(getter_AddRefs(m_tempMessageStream));
   else
@@ -1634,16 +1635,18 @@ nsresult nsMsgDBFolder::StartNewOfflineM
   return rv;
 }
 
 nsresult nsMsgDBFolder::EndNewOfflineMessage()
 {
   nsCOMPtr <nsISeekableStream> seekable;
   nsInt64 curStorePos;
   PRUint32 messageOffset;
+  PRUint32 messageSize;
+
   nsMsgKey messageKey;
 
   nsresult rv = GetDatabase();
   NS_ENSURE_SUCCESS(rv, rv);
 
   m_offlineHeader->GetMessageKey(&messageKey);
   if (m_tempMessageStream)
     seekable = do_QueryInterface(m_tempMessageStream);
@@ -1651,20 +1654,37 @@ nsresult nsMsgDBFolder::EndNewOfflineMes
   mDatabase->MarkOffline(messageKey, PR_TRUE, nsnull);
   if (seekable)
   {
     seekable->Seek(PR_SEEK_CUR, 0); // seeking causes a flush, w/o syncing
     PRInt64 tellPos;
     seekable->Tell(&tellPos);
     curStorePos = tellPos;
 
+    // N.B. This only works if we've set the offline flag for the message,
+    // so be careful about moving the call to MarkOffline above.
     m_offlineHeader->GetMessageOffset(&messageOffset);
     curStorePos -= messageOffset;
     m_offlineHeader->SetOfflineMessageSize(curStorePos);
-    m_offlineHeader->SetLineCount(m_numOfflineMsgLines);
+    m_offlineHeader->GetMessageSize(&messageSize);
+    messageSize += m_bytesAddedToLocalMsg;
+    // unix/mac has a one byte line ending, but the imap server returns
+    // crlf terminated lines.
+    if (MSG_LINEBREAK_LEN == 1)
+      messageSize -= m_numOfflineMsgLines;
+
+    // We clear the offline flag on the message if the size
+    // looks wrong.
+    if ((PRUint32) curStorePos  < messageSize)
+    {
+       mDatabase->MarkOffline(messageKey, PR_FALSE, nsnull);
+       NS_ERROR("offline message too small");
+    }
+    else
+      m_offlineHeader->SetLineCount(m_numOfflineMsgLines);
   }
 #ifdef _DEBUG
   nsCOMPtr<nsIInputStream> inputStream;
   GetOfflineStoreInputStream(getter_AddRefs(inputStream));
   if (inputStream)
     NS_ASSERTION(VerifyOfflineMessage(m_offlineHeader, inputStream),
                  "offline message doesn't start with From ");
 #endif
--- a/mailnews/base/util/nsMsgDBFolder.h
+++ b/mailnews/base/util/nsMsgDBFolder.h
@@ -183,16 +183,17 @@ protected:
   PRBool mCharsetOverride;
   PRBool mAddListener;
   PRBool mNewMessages;
   PRBool mGettingNewMessages;
   nsMsgKey mLastMessageLoaded;
 
   nsCOMPtr <nsIMsgDBHdr> m_offlineHeader;
   PRInt32 m_numOfflineMsgLines;
+  PRInt32 m_bytesAddedToLocalMsg;
   // this is currently used when we do a save as of an imap or news message..
   nsCOMPtr<nsIOutputStream> m_tempMessageStream;
 
   nsCOMPtr <nsIMsgRetentionSettings> m_retentionSettings;
   nsCOMPtr <nsIMsgDownloadSettings> m_downloadSettings;
   static NS_MSG_BASE_STATIC_MEMBER_(nsIAtom*) mFolderLoadedAtom;
   static NS_MSG_BASE_STATIC_MEMBER_(nsIAtom*) mDeleteOrMoveMsgCompletedAtom;
   static NS_MSG_BASE_STATIC_MEMBER_(nsIAtom*) mDeleteOrMoveMsgFailedAtom;
--- a/mailnews/imap/public/nsIImapMessageSink.idl
+++ b/mailnews/imap/public/nsIImapMessageSink.idl
@@ -36,32 +36,39 @@
  * ***** END LICENSE BLOCK ***** */
 
 #include "nsISupports.idl"
 #include "MailNewsTypes2.idl"
 #include "nsIImapUrl.idl"
 
 interface nsIMsgMailNewsUrl;
 
-[scriptable, uuid(F9D64900-F83B-4265-A774-C1E7006C7EC4)]
+[scriptable, uuid(33694e58-458b-4bbd-afbf-96d06f90676b)]
 
 interface nsIImapMessageSink : nsISupports {
   // set up messge download output stream
   void setupMsgWriteStream(in nsIFile aFile, in boolean aAppendDummyEnvelope);
 
-  void parseAdoptedMsgLine(in string aAdoptedMsgLine, in nsMsgKey aUidOfMsg);
-    
-  void normalEndMsgWriteStream(in nsMsgKey aUidOfMessage, in boolean aMarkMsgRead, in nsIImapUrl aImapUrl);
-    
+  /**
+   * Used by the imap protocol code to notify the core backend code about
+   * downloaded imap messages.
+   *
+   * @param aAdoptedMsgLine  a string with a lot of message lines,
+   *                         separated by native line terminators.
+   * @param aUidOfMsg        IMAP UID of the fetched message.
+   * @param aImapUrl         IMAP Url used to fetch the message.
+   */
+  void parseAdoptedMsgLine(in string aAdoptedMsgLine, in nsMsgKey aUidOfMsg,
+                           in nsIImapUrl aImapUrl);
+
+  void normalEndMsgWriteStream(in nsMsgKey aUidOfMessage,
+                               in boolean aMarkMsgRead, in nsIImapUrl aImapUrl);
+
   void abortMsgWriteStream();
 
-  attribute boolean notifyDownloadedLines;  // imap protocol doesn't notify message sink of downloaded
-                                  // lines when it has a channelListener. This forces it to,
-                                  // even if there is a channel listener.
-
   void beginMessageUpload();
 
   /**
    *  Notify the message sink that one or more flags have changed
    *  For Condstore servers, also update the highestMod Sequence
    *  @param   aFlags         - The new flags for the message
    *  @param   aMessageKey    - The UID of the message that changed
    *  @param   aHighestModSeq - The highest mod seq the parser has seen
--- a/mailnews/imap/public/nsIImapUrl.idl
+++ b/mailnews/imap/public/nsIImapUrl.idl
@@ -55,17 +55,17 @@ typedef long nsImapContentModifiedType;
 interface nsImapContentModifiedTypes
 {
   const long IMAP_CONTENT_NOT_MODIFIED = 0;
   const long IMAP_CONTENT_MODIFIED_VIEW_INLINE = 1;
   const long IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS = 2;
   const long IMAP_CONTENT_FORCE_CONTENT_NOT_MODIFIED = 3;
 } ;
 
-[scriptable, uuid(1d1b0a42-af5e-40fa-a0d4-2455179083ce)]
+[scriptable, uuid(2a85ea47-480b-4ed7-97ec-d96baa3f94ef)]
 interface nsIImapUrl : nsISupports
 {
   ///////////////////////////////////////////////////////////////////////////////
   // Getters and Setters for the imap specific event sinks to bind to to the url
   ///////////////////////////////////////////////////////////////////////////////
   attribute nsIImapMailFolderSink imapMailFolderSink;
   attribute nsIImapMessageSink imapMessageSink;
   attribute nsIImapServerSink imapServerSink;
@@ -102,19 +102,30 @@ interface nsIImapUrl : nsISupports
   attribute nsImapContentModifiedType contentModified;
   attribute boolean fetchPartsOnDemand;  // set to true if we're fetching a msg for display and want to not download parts
   attribute boolean msgLoadingFromCache; // true if this msg load is coming from a cache, so we can know to mark it read
   attribute boolean externalLinkUrl; // true if we ran this url because the user clicked on a link.
   attribute boolean validUrl; // false if we couldn't parse url for whatever reason.
   attribute nsISupports copyState;
   attribute nsIFile msgFile;
   attribute nsIImapMockChannel mockChannel;
-  attribute boolean shouldStoreMsgOffline; // set to true if we should store the msg for offline use if we can,
-                                           // i.e., we're not doing mime parts on demand.
-  attribute boolean rerunningUrl; // server disconnected first time so we're retrying
+  /**
+   * Set to true if we should store the msg(s) for offline use if we can,
+   * e.g., we're fetching a message and the folder is configured for offline
+   * use and we're not doing mime parts on demand.
+   */
+  attribute boolean storeResultsOffline;
+  /**
+   * If we fallback from fetching by parts to fetching the whole message,
+   *  because all the parts were inline, this tells us we should store
+   * the message offline.
+   */
+  attribute boolean storeOfflineOnFallback;
+  /// Server disconnected first time so we're retrying.
+  attribute boolean rerunningUrl;
 
   /**
    * @{
    * This is used to tell the runner of the url more about the status of
    * the command, beyond whether it was successful or not. For example,
    * subtracting flags from a UID that doesn't exist isn't an error
    * (the server returns OK), but the backend code may want to know about it.
    */
--- a/mailnews/imap/src/nsImapMailFolder.cpp
+++ b/mailnews/imap/src/nsImapMailFolder.cpp
@@ -214,17 +214,16 @@ nsImapMailFolder::nsImapMailFolder() :
     m_folderIsNamespace(PR_FALSE),
     m_folderNeedsSubscribing(PR_FALSE),
     m_folderNeedsAdded(PR_FALSE),
     m_folderNeedsACLListed(PR_TRUE),
     m_performingBiff(PR_FALSE),
     m_folderQuotaCommandIssued(PR_FALSE),
     m_folderQuotaDataIsValid(PR_FALSE),
     m_updatingFolder(PR_FALSE),
-    m_downloadMessageForOfflineUse(PR_FALSE),
     m_downloadingFolderForOfflineUse(PR_FALSE),
     m_folderQuotaUsedKB(0),
     m_folderQuotaMaxKB(0),
     m_applyIncomingFilters(PR_FALSE)
 {
   MOZ_COUNT_CTOR(nsImapMailFolder); // double count these for now.
 
   if (mImapHdrDownloadedAtom == nsnull)
@@ -4213,77 +4212,63 @@ NS_IMETHODIMP nsImapMailFolder::Download
 
     rv = AcquireSemaphore(static_cast<nsIMsgImapMailFolder*>(this));
     if (NS_FAILED(rv))
     {
       m_downloadingFolderForOfflineUse = PR_FALSE;
       ThrowAlertMsg("operationFailedFolderBusy", msgWindow);
       return rv;
     }
-    SetNotifyDownloadedLines(PR_TRUE);
     nsCOMPtr<nsIImapService> imapService = do_GetService(NS_IMAPSERVICE_CONTRACTID, &rv);
-    NS_ENSURE_SUCCESS(rv,rv);
-
-    // selecting the folder with m_downloadingFolderForOfflineUse true will cause
-    // us to fetch any message bodies we don't have.
-    rv = imapService->SelectFolder(m_thread, this, listener, msgWindow, nsnull);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Selecting the folder with nsIImapUrl::shouldStoreMsgOffline true will
+    // cause us to fetch any message bodies we don't have.
+    rv = imapService->SelectFolder(m_thread, this, listener, msgWindow,
+                                   getter_AddRefs(runningURI));
     if (NS_SUCCEEDED(rv))
+    {
+      nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(runningURI));
+      if (imapUrl)
+        imapUrl->SetStoreResultsOffline(PR_TRUE);
       m_urlRunning = PR_TRUE;
+    }
   }
   else
     rv = NS_MSG_FOLDER_UNREADABLE;
   return rv;
 }
 
 NS_IMETHODIMP
-nsImapMailFolder::GetNotifyDownloadedLines(PRBool *notifyDownloadedLines)
-{
-  NS_ENSURE_ARG(notifyDownloadedLines);
-  *notifyDownloadedLines = m_downloadMessageForOfflineUse;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsImapMailFolder::SetNotifyDownloadedLines(PRBool notifyDownloadedLines)
-{
-  // ignore this if we're downloading the whole folder and someone says
-  // to turn off downloading for offline use, which can happen if a 3rd party
-  // app tries to stream a message while we're downloading for offline use.
-  if (!notifyDownloadedLines && m_downloadingFolderForOfflineUse)
-    return NS_OK;
-  m_downloadMessageForOfflineUse = notifyDownloadedLines;
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-nsImapMailFolder::ParseAdoptedMsgLine(const char *adoptedMessageLine, nsMsgKey uidOfMessage)
-{
+nsImapMailFolder::ParseAdoptedMsgLine(const char *adoptedMessageLine,
+                                      nsMsgKey uidOfMessage,
+                                      nsIImapUrl *aImapUrl)
+{
+  NS_ENSURE_ARG_POINTER(aImapUrl);
   PRUint32 count = 0;
   nsresult rv;
   // remember the uid of the message we're downloading.
   m_curMsgUid = uidOfMessage;
-  if (m_downloadMessageForOfflineUse && !m_offlineHeader)
+  if (!m_offlineHeader)
   {
     GetMessageHeader(uidOfMessage, getter_AddRefs(m_offlineHeader));
     rv = StartNewOfflineMessage();
   }
   // adoptedMessageLine is actually a string with a lot of message lines, separated by native line terminators
   // we need to count the number of MSG_LINEBREAK's to determine how much to increment m_numOfflineMsgLines by.
-  if (m_downloadMessageForOfflineUse)
-  {
-    const char *nextLine = adoptedMessageLine;
-    do
-    {
-      m_numOfflineMsgLines++;
-      nextLine = PL_strstr(nextLine, MSG_LINEBREAK);
-      if (nextLine)
-        nextLine += MSG_LINEBREAK_LEN;
-    }
-    while (nextLine && *nextLine);
-  }
+  const char *nextLine = adoptedMessageLine;
+  do
+  {
+    m_numOfflineMsgLines++;
+    nextLine = PL_strstr(nextLine, MSG_LINEBREAK);
+    if (nextLine)
+      nextLine += MSG_LINEBREAK_LEN;
+  }
+  while (nextLine && *nextLine);
+
   if (m_tempMessageStream)
   {
     nsCOMPtr <nsISeekableStream> seekable (do_QueryInterface(m_tempMessageStream));
     if (seekable)
       seekable->Seek(PR_SEEK_END, 0);
     rv = m_tempMessageStream->Write(adoptedMessageLine,
                 PL_strlen(adoptedMessageLine), &count);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -4767,20 +4752,23 @@ nsImapMailFolder::OnStartRunningUrl(nsIU
 
 NS_IMETHODIMP
 nsImapMailFolder::OnStopRunningUrl(nsIURI *aUrl, nsresult aExitCode)
 {
   nsresult rv;
   PRBool endedOfflineDownload = PR_FALSE;
   m_urlRunning = PR_FALSE;
   m_updatingFolder = PR_FALSE;
-  if (m_downloadingFolderForOfflineUse)
+  nsCOMPtr <nsIImapUrl> imapUrl = do_QueryInterface(aUrl, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+  PRBool downloadingForOfflineUse;
+  imapUrl->GetStoreResultsOffline(&downloadingForOfflineUse);
+  if (downloadingForOfflineUse)
   {
     ReleaseSemaphore(static_cast<nsIMsgImapMailFolder*>(this));
-    m_downloadingFolderForOfflineUse = PR_FALSE;
     endedOfflineDownload = PR_TRUE;
     EndOfflineDownload();
   }
   nsCOMPtr<nsIMsgMailSession> session = do_GetService(NS_MSGMAILSESSION_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
   if (aUrl)
   {
     nsCOMPtr<nsIMsgWindow> msgWindow;
@@ -4800,17 +4788,16 @@ nsImapMailFolder::OnStopRunningUrl(nsIUR
    if (imapUrl)
    {
       DisplayStatusMsg(imapUrl, EmptyString());
       nsImapAction imapAction = nsIImapUrl::nsImapTest;
       imapUrl->GetImapAction(&imapAction);
       if (imapAction == nsIImapUrl::nsImapMsgFetch || imapAction == nsIImapUrl::nsImapMsgDownloadForOffline)
       {
         ReleaseSemaphore(static_cast<nsIMsgImapMailFolder*>(this));
-        SetNotifyDownloadedLines(PR_FALSE);
         if (!endedOfflineDownload)
           EndOfflineDownload();
       }
 
       // Notify move, copy or delete (online operations)
       // Not sure whether nsImapDeleteMsg is even used, deletes in all three models use nsImapAddMsgFlags.
       nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID));
       if (notifier && m_copyState)
@@ -5386,20 +5373,18 @@ nsImapMailFolder::HeaderFetchCompleted(n
     PRBool notifiedBodies = PR_FALSE;
     if (m_downloadingFolderForOfflineUse || autoSyncOfflineStores ||
         autoDownloadNewHeaders)
     {
       nsTArray<nsMsgKey> keysToDownload;
       GetBodysToDownload(&keysToDownload);
       if (!keysToDownload.IsEmpty())
       {
-        SetNotifyDownloadedLines(PR_TRUE);
-
         // this is the case when DownloadAllForOffline is called.
-        if (m_downloadingFolderForOfflineUse)
+        if (m_downloadingFolderForOfflineUse || autoDownloadNewHeaders)
         {
           notifiedBodies = PR_TRUE;
           aProtocol->NotifyBodysToDownload(keysToDownload.Elements(), keysToDownload.Length());
         }
         else
         {
           // create auto-sync state object lazily
           InitAutoSyncState();
--- a/mailnews/imap/src/nsImapMailFolder.h
+++ b/mailnews/imap/src/nsImapMailFolder.h
@@ -501,17 +501,16 @@ protected:
   PRPackedBool m_applyIncomingFilters; // apply filters to this folder, even if not the inbox
   nsMsgIMAPFolderACL *m_folderACL;
   PRUint32     m_aclFlags;
   PRUint32     m_supportedUserFlags;
 
   static nsIAtom* mImapHdrDownloadedAtom;
 
   // offline imap support
-  PRBool m_downloadMessageForOfflineUse;
   PRBool m_downloadingFolderForOfflineUse;
   
   // auto-sync (preemptive download) support
   nsRefPtr<nsAutoSyncState> m_autoSyncStateObj;
   
   // Quota support
   nsCString m_folderQuotaRoot;
   PRUint32 m_folderQuotaUsedKB;
--- a/mailnews/imap/src/nsImapProtocol.cpp
+++ b/mailnews/imap/src/nsImapProtocol.cpp
@@ -2373,16 +2373,17 @@ void nsImapProtocol::ProcessSelectedStat
             SetProgressString(IMAP_FOLDER_RECEIVING_MESSAGE_OF);
 
             m_progressIndex = 0;
             m_progressCount = CountMessagesInIdString(messageIdString.get());
 
             // we need to set this so we'll get the msg from the memory cache.
             if (m_imapAction == nsIImapUrl::nsImapMsgFetchPeek)
               SetContentModified(IMAP_CONTENT_NOT_MODIFIED);
+
             FetchMessage(messageIdString,
               (m_imapAction == nsIImapUrl::nsImapMsgPreview)
               ? kBodyStart : kEveryThingRFC822Peek);
             if (m_imapAction == nsIImapUrl::nsImapMsgPreview)
               HeaderFetchCompleted();
             SetProgressString(0);
           }
           else
@@ -2449,40 +2450,41 @@ void nsImapProtocol::ProcessSelectedStat
               // need to set a flag in the url, I guess, equiv to allow_content_changed.
               PRBool allowedToBreakApart = PR_TRUE; // (ce  && !DeathSignalReceived()) ? ce->URL_s->allow_content_change : PR_FALSE;
               PRBool mimePartSelectorDetected;
               PRBool urlOKToFetchByParts = PR_FALSE;
               m_runningUrl->GetMimePartSelectorDetected(&mimePartSelectorDetected);
               m_runningUrl->GetFetchPartsOnDemand(&urlOKToFetchByParts);
 
               if (urlOKToFetchByParts &&
-                allowedToBreakApart &&
-                !GetShouldFetchAllParts() &&
-                GetServerStateParser().ServerHasIMAP4Rev1Capability() /* &&
+                  allowedToBreakApart &&
+                  !GetShouldFetchAllParts() &&
+                  GetServerStateParser().ServerHasIMAP4Rev1Capability() /* &&
                 !mimePartSelectorDetected */)  // if a ?part=, don't do BS.
               {
                 // OK, we're doing bodystructure
 
                 // Before fetching the bodystructure, let's check our body shell cache to see if
                 // we already have it around.
                 nsIMAPBodyShell *foundShell = NULL;
                 IMAP_ContentModifiedType modType = GetShowAttachmentsInline() ?
                   IMAP_CONTENT_MODIFIED_VIEW_INLINE :
                   IMAP_CONTENT_MODIFIED_VIEW_AS_LINKS ;
 
+                PRBool wasStoringMsgOffline;
+                m_runningUrl->GetStoreResultsOffline(&wasStoringMsgOffline);
+                m_runningUrl->SetStoreOfflineOnFallback(wasStoringMsgOffline);
+                m_runningUrl->SetStoreResultsOffline(PR_FALSE);
                 nsCOMPtr<nsIMsgMailNewsUrl> mailurl = do_QueryInterface(m_runningUrl);
                 if (mailurl)
                 {
                   mailurl->SetAddToMemoryCache(PR_FALSE);
                   // need to proxy this over to the ui thread
                   if (m_imapMessageSink)
-                  {
-                    m_imapMessageSink->SetNotifyDownloadedLines(PR_FALSE);
                     m_imapMessageSink->SetImageCacheSessionForUrl(mailurl);
-                  }
 
                 }
                 SetContentModified(modType);  // This will be looked at by the cache
                 if (bMessageIdsAreUids)
                 {
                   res = m_hostSessionList->FindShellInCacheForHost(GetImapServerKey(),
                     GetServerStateParser().GetSelectedMailboxName(), messageIdString.get(), modType, &foundShell);
                   if (foundShell)
@@ -3159,19 +3161,18 @@ void nsImapProtocol::FetchMsgAttribute(c
 // this routine is used to fetch a message or messages, or headers for a
 // message...
 
 void nsImapProtocol::FallbackToFetchWholeMsg(const nsCString &messageId, PRUint32 messageSize)
 {
   if (m_imapMessageSink && m_runningUrl)
   {
     PRBool shouldStoreMsgOffline;
-    m_runningUrl->GetShouldStoreMsgOffline(&shouldStoreMsgOffline);
-    if (shouldStoreMsgOffline)
-      m_imapMessageSink->SetNotifyDownloadedLines(PR_TRUE);
+    m_runningUrl->GetStoreOfflineOnFallback(&shouldStoreMsgOffline);
+    m_runningUrl->SetStoreResultsOffline(shouldStoreMsgOffline);
   }
   FetchTryChunking(messageId, kEveryThingRFC822, PR_TRUE, NULL, messageSize, PR_TRUE);
 }
 
 void
 nsImapProtocol::FetchMessage(const nsCString &messageIds, 
                              nsIMAPeFetchFields whatToFetch,
                              const char *fetchModifier,
@@ -3571,25 +3572,22 @@ nsImapProtocol::PostLineDownLoadEvent(co
       if (m_channelOutputStream)
       {
         nsresult rv = m_channelOutputStream->Write(line, PL_strlen(line), &count);
         if (NS_SUCCEEDED(rv))
         {
           nsCOMPtr<nsIRequest> request = do_QueryInterface(m_mockChannel);
           m_channelListener->OnDataAvailable(request, m_channelContext, m_channelInputStream, 0, count);
         }
-          // here is where we should echo the line to the local folder copy of an online message
       }
-      if (m_imapMessageSink)
-        m_imapMessageSink->GetNotifyDownloadedLines(&echoLineToMessageSink);
+      if (m_runningUrl)
+        m_runningUrl->GetStoreResultsOffline(&echoLineToMessageSink);
     }
     if (m_imapMessageSink && line && echoLineToMessageSink && !GetPseudoInterrupted())
-    {
-      m_imapMessageSink->ParseAdoptedMsgLine(line, uidOfMessage);
-    }
+      m_imapMessageSink->ParseAdoptedMsgLine(line, uidOfMessage, m_runningUrl);
   }
   // ***** We need to handle the pseudo interrupt here *****
 }
 
 // Handle a line seen by the parser.
 // * The argument |lineCopy| must be nsnull or should contain the same string as
 //   |line|.  |lineCopy| will be modified.
 // * A line may be passed by parts, e.g., "part1 part2\r\n" may be passed as
--- a/mailnews/imap/src/nsImapService.cpp
+++ b/mailnews/imap/src/nsImapService.cpp
@@ -525,17 +525,17 @@ NS_IMETHODIMP nsImapService::DisplayMess
       PRBool useMimePartsOnDemand = gMIMEOnDemand;
       PRBool shouldStoreMsgOffline = PR_FALSE;
       PRBool hasMsgOffline = PR_FALSE;
 
       nsCOMPtr<nsIMsgIncomingServer> aMsgIncomingServer;
 
       if (imapMessageSink)
         imapMessageSink->GetMessageSizeFromDB(msgKey.get(), &messageSize);
-      
+
       msgurl->SetMsgWindow(aMsgWindow);
 
       rv = msgurl->GetServer(getter_AddRefs(aMsgIncomingServer));
 
       if (NS_SUCCEEDED(rv) && aMsgIncomingServer)
       {
         nsCOMPtr<nsIImapIncomingServer> aImapServer(do_QueryInterface(aMsgIncomingServer, &rv));
         if (NS_SUCCEEDED(rv) && aImapServer)
@@ -553,38 +553,20 @@ NS_IMETHODIMP nsImapService::DisplayMess
           useMimePartsOnDemand = PR_FALSE;
       }
 
       if (folder)
       {
         folder->ShouldStoreMsgOffline(key, &shouldStoreMsgOffline);
         folder->HasMsgOffline(key, &hasMsgOffline);
       }
-
-      if (!useMimePartsOnDemand || (messageSize < (uint32) gMIMEOnDemandThreshold))
-        //                allowedToBreakApart && 
-        //              !GetShouldFetchAllParts() &&
-        //            GetServerStateParser().ServerHasIMAP4Rev1Capability() &&
-      {
-        imapUrl->SetFetchPartsOnDemand(PR_FALSE);
-        // for now, lets try not adding these 
-        msgurl->SetAddToMemoryCache(PR_TRUE);
-      }
-      else
-      {
-        // whenever we are displaying a message, we want to add it to the memory cache..
-        imapUrl->SetFetchPartsOnDemand(PR_TRUE);
-        // if we happen to fetch the whole message, note in the url
-        // whether we want to store this message offline.
-        imapUrl->SetShouldStoreMsgOffline(shouldStoreMsgOffline);
-        shouldStoreMsgOffline = PR_FALSE; // if we're fetching by parts, don't store offline
-        msgurl->SetAddToMemoryCache(PR_FALSE);
-      }
-      if (imapMessageSink && !hasMsgOffline)
-        imapMessageSink->SetNotifyDownloadedLines(shouldStoreMsgOffline);
+      imapUrl->SetStoreResultsOffline(shouldStoreMsgOffline);
+      msgurl->SetAddToMemoryCache(!hasMsgOffline);
+      imapUrl->SetFetchPartsOnDemand(
+        useMimePartsOnDemand && messageSize >= (PRUint32) gMIMEOnDemandThreshold);
 
       if (hasMsgOffline)
         msgurl->SetMsgIsInLocalCache(PR_TRUE);
 
       nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
       // Should the message fetch force a peek or a traditional fetch?
       // Force peek if there is a delay in marking read (or no auto-marking at all).
       // This is because a FETCH (BODY[]) will implicitly set tha \Seen flag on the msg,
@@ -615,28 +597,26 @@ nsresult nsImapService::FetchMimePart(ns
                                       nsIURI **aURL,
                                       nsISupports *aDisplayConsumer, 
                                       const nsACString &messageIdentifierList,
                                       const nsACString &mimePart) 
 {
   NS_ENSURE_ARG_POINTER(aImapUrl);
   NS_ENSURE_ARG_POINTER(aImapMailFolder);
   NS_ENSURE_ARG_POINTER(aImapMessage);
-  
+
   // create a protocol instance to handle the request.
   // NOTE: once we start working with multiple connections, this step will be much more complicated...but for now
   // just create a connection and process the request.
-  
-  nsresult rv;
   nsCAutoString urlSpec;
-  rv = SetImapUrlSink(aImapMailFolder, aImapUrl);
+  nsresult rv = SetImapUrlSink(aImapMailFolder, aImapUrl);
   nsImapAction actionToUse = aImapAction;
   if (actionToUse == nsImapUrl::nsImapOpenMimePart)
     actionToUse = nsIImapUrl::nsImapMsgFetch;
-  
+
   nsCOMPtr<nsIMsgMailNewsUrl> msgurl (do_QueryInterface(aImapUrl));
   if (aImapMailFolder && msgurl && !messageIdentifierList.IsEmpty())
   {
     PRBool useLocalCache = PR_FALSE;
     aImapMailFolder->HasMsgOffline(atoi(nsCString(messageIdentifierList).get()), &useLocalCache);
     msgurl->SetMsgIsInLocalCache(useLocalCache);
   }
   rv = aImapUrl->SetImapMessageSink(aImapMessage);
@@ -1233,19 +1213,17 @@ NS_IMETHODIMP nsImapService::StreamMessa
         }
       }
 
       imapUrl->SetFetchPartsOnDemand(PR_FALSE);
       msgurl->SetAddToMemoryCache(PR_TRUE);
 
       PRBool shouldStoreMsgOffline = PR_FALSE;
       folder->ShouldStoreMsgOffline(key, &shouldStoreMsgOffline);
-      if (imapMessageSink && !hasMsgOffline)
-        imapMessageSink->SetNotifyDownloadedLines(shouldStoreMsgOffline);
-      
+      imapUrl->SetStoreResultsOffline(shouldStoreMsgOffline);
       rv = GetMessageFromUrl(imapUrl, nsIImapUrl::nsImapMsgFetchPeek, folder,
                              imapMessageSink, aMsgWindow, aConsumer,
                              aConvertData, aURL);
     }
   }
   return rv;
 }
 
@@ -3226,24 +3204,27 @@ NS_IMETHODIMP nsImapService::DownloadMes
   char hierarchyDelimiter = GetHierarchyDelimiter(aFolder);
   rv = CreateStartOfImapUrl(EmptyCString(), getter_AddRefs(imapUrl), aFolder, nsnull,
                             urlSpec, hierarchyDelimiter);
   if (NS_SUCCEEDED(rv) && imapUrl)
   {
     nsCOMPtr<nsIURI> runningURI;
     // need to pass in stream listener in order to get the channel created correctly
     nsCOMPtr<nsIImapMessageSink> imapMessageSink(do_QueryInterface(aFolder, &rv));
-    imapMessageSink->SetNotifyDownloadedLines(PR_TRUE);
-    rv = FetchMessage(imapUrl, nsImapUrl::nsImapMsgDownloadForOffline,aFolder, imapMessageSink, 
-                      aMsgWindow, nsnull, messageIds, PR_FALSE, EmptyCString(), getter_AddRefs(runningURI));
+    rv = FetchMessage(imapUrl, nsImapUrl::nsImapMsgDownloadForOffline,aFolder,
+                      imapMessageSink, aMsgWindow, nsnull, messageIds,
+                      PR_FALSE, EmptyCString(), getter_AddRefs(runningURI));
     if (runningURI && aUrlListener)
     {
       nsCOMPtr<nsIMsgMailNewsUrl> msgurl (do_QueryInterface(runningURI));
+      nsCOMPtr<nsIImapUrl> imapUrl(do_QueryInterface(runningURI));
       if (msgurl)
         msgurl->RegisterListener(aUrlListener);
+      if (imapUrl)
+        imapUrl->SetStoreResultsOffline(PR_TRUE);
     }
   }
   return rv;
 }
 
 NS_IMETHODIMP nsImapService::MessageURIToMsgHdr(const char *uri, nsIMsgDBHdr **aRetVal)
 {
   NS_ENSURE_ARG_POINTER(uri);
--- a/mailnews/imap/src/nsImapUrl.cpp
+++ b/mailnews/imap/src/nsImapUrl.cpp
@@ -76,17 +76,18 @@ nsImapUrl::nsImapUrl()
   m_listOfMessageIds = nsnull;
   m_tokenPlaceHolder = nsnull;
   m_searchCriteriaString = nsnull;
   m_idsAreUids = PR_FALSE;
   m_mimePartSelectorDetected = PR_FALSE;
   m_allowContentChange = PR_TRUE;  // assume we can do MPOD.
   m_fetchPartsOnDemand = PR_FALSE; // but assume we're not doing it :-)
   m_msgLoadingFromCache = PR_FALSE;
-  m_shouldStoreMsgOffline = PR_FALSE;
+  m_storeResultsOffline = PR_FALSE;
+  m_storeOfflineOnFallback = PR_FALSE;
   m_rerunningUrl = PR_FALSE;
   m_externalLinkUrl = PR_TRUE; // we'll start this at true, and set it false in nsImapService::CreateStartOfImapUrl
   m_contentModified = IMAP_CONTENT_NOT_MODIFIED;
   m_validUrl = PR_TRUE;  // assume the best.
   m_flags = 0;
   m_extraStatus = ImapStatusNone;
   m_onlineSubDirSeparator = '/';
 
@@ -1511,26 +1512,39 @@ NS_IMETHODIMP nsImapUrl::GetCharsetOverR
 }
 
 NS_IMETHODIMP nsImapUrl::SetCharsetOverRide(const char * aCharacterSet)
 {
   mCharsetOverride = aCharacterSet;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsImapUrl::GetShouldStoreMsgOffline(PRBool *aShouldStoreMsgOffline)
+NS_IMETHODIMP nsImapUrl::GetStoreResultsOffline(PRBool *aStoreResultsOffline)
 {
-  NS_ENSURE_ARG_POINTER(aShouldStoreMsgOffline);
-  *aShouldStoreMsgOffline = m_shouldStoreMsgOffline;
+  NS_ENSURE_ARG_POINTER(aStoreResultsOffline);
+  *aStoreResultsOffline = m_storeResultsOffline;
   return NS_OK;
 }
 
-NS_IMETHODIMP nsImapUrl::SetShouldStoreMsgOffline(PRBool aShouldStoreMsgOffline)
+NS_IMETHODIMP nsImapUrl::SetStoreResultsOffline(PRBool aStoreResultsOffline)
+{
+  m_storeResultsOffline = aStoreResultsOffline;
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::GetStoreOfflineOnFallback(PRBool *aStoreOfflineOnFallback)
 {
-  m_shouldStoreMsgOffline = aShouldStoreMsgOffline;
+  NS_ENSURE_ARG_POINTER(aStoreOfflineOnFallback);
+  *aStoreOfflineOnFallback = m_storeOfflineOnFallback;
+  return NS_OK;
+}
+
+NS_IMETHODIMP nsImapUrl::SetStoreOfflineOnFallback(PRBool aStoreOfflineOnFallback)
+{
+  m_storeOfflineOnFallback = aStoreOfflineOnFallback;
   return NS_OK;
 }
 
 NS_IMETHODIMP nsImapUrl::GetMessageHeader(nsIMsgDBHdr ** aMsgHdr)
 {
   nsCString uri;
   nsresult rv = GetUri(getter_Copies(uri));
   NS_ENSURE_SUCCESS(rv, rv);
--- a/mailnews/imap/src/nsImapUrl.h
+++ b/mailnews/imap/src/nsImapUrl.h
@@ -117,17 +117,19 @@ protected:
   PRPackedBool m_validUrl;
   PRPackedBool m_runningUrl;
   PRPackedBool m_idsAreUids;
   PRPackedBool m_mimePartSelectorDetected;
   PRPackedBool m_allowContentChange;  // if PR_FALSE, we can't use Mime parts on demand
   PRPackedBool m_fetchPartsOnDemand; // if PR_TRUE, we should fetch leave parts on server.
   PRPackedBool m_msgLoadingFromCache; // if PR_TRUE, we might need to mark read on server
   PRPackedBool m_externalLinkUrl; // if PR_TRUE, we're running this url because the user
-  PRPackedBool m_shouldStoreMsgOffline;
+  // True if the fetch results should be put in the offline store.
+  PRPackedBool m_storeResultsOffline;
+  PRPackedBool m_storeOfflineOnFallback;
   PRPackedBool m_rerunningUrl; // first attempt running this failed with connection error; retrying
   nsImapContentModifiedType  m_contentModified;
 
   PRInt32    m_extraStatus;
 
   nsCString  m_userName;
   nsCString  m_serverKey;
   // event sinks
new file mode 100644
--- /dev/null
+++ b/mailnews/imap/test/unit/test_imapStoreMsgOffline.js
@@ -0,0 +1,265 @@
+/**
+ * This test checks if the imap protocol code saves message to
+ * offline stores correctly, when we fetch the message for display.
+ * It checks:
+ *   - Normal messages, no attachments.
+ *   - Message with inline attachment (e.g., image)
+ *   - Message with non-inline attachment (e.g., .doc file)
+ *   - Message with mix of attachment types.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const nsMsgMessageFlags = Ci.nsMsgMessageFlags;
+
+var gMsgFile1 = do_get_file("../../mailnews/data/bugmail10");
+const gMsgId1 = "200806061706.m56H6RWT004933@mrapp54.mozilla.org";
+var gMsgFile2 = do_get_file("../../mailnews/data/image-attach-test");
+const gMsgId2 = "4A947F73.5030709@xxx.com";
+var gMsgFile3 = do_get_file("../../mailnews/data/external-attach-test");
+const gMsgId3 = "876TY.5030709@xxx.com";
+
+// We use this as a display consumer
+var streamListener =
+{
+  _data: "",
+
+  QueryInterface:
+    XPCOMUtils.generateQI([Ci.nsIStreamListener, Ci.nsIRequestObserver]),
+
+  // nsIRequestObserver
+  onStartRequest: function(aRequest, aContext) {
+  },
+  onStopRequest: function(aRequest, aContext, aStatusCode) {
+    do_check_eq(aStatusCode, 0);
+  },
+
+  // nsIStreamListener
+  onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
+    let scriptStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                         .createInstance(Ci.nsIScriptableInputStream);
+
+    scriptStream.init(aInputStream);
+
+    scriptStream.read(aCount);
+  }
+};
+
+// Adds some messages directly to a mailbox (eg new mail)
+function addMessagesToServer(messages, mailbox, localFolder)
+{
+  let ioService = Cc["@mozilla.org/network/io-service;1"]
+                    .getService(Ci.nsIIOService);
+
+  // For every message we have, we need to convert it to a file:/// URI
+  messages.forEach(function (message)
+  {
+    let URI = ioService.newFileURI(message.file).QueryInterface(Ci.nsIFileURL);
+    message.spec = URI.spec;
+  });
+
+  // Create the imapMessages and store them on the mailbox
+  messages.forEach(function (message)
+  {
+    mailbox.addMessage(new imapMessage(message.spec, mailbox.uidnext++, []));
+  });
+}
+
+var incomingServer, server;
+function run_test() {
+  // This is before any of the actual tests, so...
+  gTest = 0;
+
+  // 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.setBoolPref("mail.server.server1.autosync_offline_stores", false);
+  prefBranch.setBoolPref("mail.server.server1.offline_download", true);
+  // make small threshhold for mpod so our test messages don't have to be big.
+  // XXX We can't set this pref until the fake server supports body structure.
+  // So for now, we'll leave it at the default value, which is larger than any of
+  // our test messages.
+  // prefBranch.setIntPref("mail.imap.mime_parts_on_demand_threshold", 3000);
+
+  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;
+
+  gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+  gMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"]
+                  .createInstance(Components.interfaces.nsIMsgWindow);
+
+  // Get the server list...
+  gIMAPIncomingServer.performExpand(null);
+
+  gRootFolder = gIMAPIncomingServer.rootFolder;
+  gIMAPInbox = gRootFolder.getChildNamed("INBOX");
+  let msgImapFolder = 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.
+  msgImapFolder.hierarchyDelimiter = '/';
+  msgImapFolder.verifiedAsOnlineFolder = true;
+
+
+  // Add a couple of messages to the INBOX
+  // this is synchronous, afaik
+  addMessagesToServer([{file: gMsgFile1, messageId: gMsgId1},
+                        {file: gMsgFile2, messageId: gMsgId2},
+                        {file: gMsgFile3, messageId: gMsgId3},
+//                         {file: gMsgFile5, messageId: gMsgId5},
+                      ],
+                        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);
+}
+
+var gIMAPService;
+
+const gTestArray =
+[
+  function updateFolder() {
+    gIMAPInbox.updateFolderWithListener(null, URLListener);
+  },
+  function selectFirstMsg() {
+
+  // We postpone creating the imap service until after we've set the prefs
+  // that it reads on its startup.
+  gIMAPService = Cc["@mozilla.org/messenger/messageservice;1?type=imap"]
+                       .getService(Ci.nsIMsgMessageService);
+
+    let db = gIMAPInbox.msgDatabase;
+    let msg1 = db.getMsgHdrForMessageID(gMsgId1);
+    let url = new Object;
+    gIMAPService.DisplayMessage(gIMAPInbox.getUriForMsg(msg1),
+                                            streamListener,
+                                            null,
+                                            URLListener,
+                                            null,
+                                            url);
+  },
+  function select2ndMsg() {
+    let msg1 = gIMAPInbox.msgDatabase.getMsgHdrForMessageID(gMsgId1);
+    do_check_neq(msg1.flags & nsMsgMessageFlags.Offline, 0);
+    let db = gIMAPInbox.msgDatabase;
+    let msg2 = db.getMsgHdrForMessageID(gMsgId2);
+    let url = new Object;
+    gIMAPService.DisplayMessage(gIMAPInbox.getUriForMsg(msg2),
+                                            streamListener,
+                                            null,
+                                            URLListener,
+                                            null,
+                                            url);
+  },
+  function select3rdMsg() {
+    let msg2 = gIMAPInbox.msgDatabase.getMsgHdrForMessageID(gMsgId2);
+    do_check_neq(msg2.flags & nsMsgMessageFlags.Offline, 0);
+    let db = gIMAPInbox.msgDatabase;
+    let msg3 = db.getMsgHdrForMessageID(gMsgId3);
+    let url = new Object;
+    gIMAPService.DisplayMessage(gIMAPInbox.getUriForMsg(msg3),
+                                            streamListener,
+                                            null,
+                                            URLListener,
+                                            null,
+                                            url);
+  },
+  function verify3rdMsg() {
+    let msg3 = gIMAPInbox.msgDatabase.getMsgHdrForMessageID(gMsgId3);
+    // can't turn this on because our fake server doesn't support body structure.
+//    do_check_eq(msg3.flags & nsMsgMessageFlags.Offline, 0);
+    do_timeout(0, "doTest(++gCurTestNum)");
+  }
+]
+
+function doTest(test)
+{
+  if (test <= gTestArray.length)
+  {
+    dump("Doing test " + test + "\n");
+    gCurTestNum = test;
+
+    var testFn = gTestArray[test - 1];
+    // Set a limit of ten 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) {
+      gServer.stop();
+      do_throw ('TEST FAILED ' + ex);
+    }
+  }
+  else
+  {
+    do_timeout(1000, "endTest();");
+  }
+}
+
+// nsIURLListener implementation - runs next test
+var URLListener =
+{
+  OnStartRunningUrl: function(aURL) {},
+  OnStopRunningUrl: function(aURL, aStatus)
+  {
+    dump("in OnStopRunningURL " + gCurTestNum + "\n");
+    do_check_eq(aStatus, 0);
+    do_timeout(0, "doTest(++gCurTestNum);");
+  }
+}
+
+function endTest()
+{
+  // Cleanup, null out everything, close all cached connections and stop the
+  // server
+//  gMessages.clear();
+  gMessenger = null;
+  gMsgWindow = null;
+  gRootFolder = null;
+  gIMAPInbox = null;
+  gIMAPTrashFolder = null;
+  gServer.resetTest();
+  gIMAPIncomingServer.closeCachedConnections();
+  gIMAPIncomingServer = null;
+  gLocalInboxFolder = null;
+  gLocalIncomingServer = null;
+  gServer.performTest();
+  gServer.stop();
+  let thread = gThreadManager.currentThread;
+  while (thread.hasPendingEvents())
+    thread.processNextEvent(true);
+
+  do_test_finished(); // for the one in run_test()
+}
new file mode 100644
--- /dev/null
+++ b/mailnews/test/data/external-attach-test
@@ -0,0 +1,63 @@
+From - Fri Aug 26 16:47:38 2008
+X-Identity-Key: id2
+Message-ID: <876TY.5030709@xxx.com>
+Date: Tue, 26 Aug 2009 17:18:59 -0700
+From: Foo Bar <xxx@xxx.com>
+Reply-To: xxx@xxx.com
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.3pre) Gecko/20090817 Shredder/3.0b4pre
+MIME-Version: 1.0
+To: asfd@asdf
+Subject: external attach test
+Content-Type: multipart/mixed;
+ boundary="------------080903080202020605050108"
+
+This is a multi-part message in MIME format.
+--------------080903080202020605050108
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+
+<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <title>image attach test</title>
+</head>
+<body bgcolor="#ffffff" text="#000000">
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890<br>
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+</body>
+</html>
+
+--------------080903080202020605050108
+Content-Type: application/pdf;
+ name="check.exe"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="check.pdf"
+
+R0lGODlhDQANAIMAAAAAAICAgP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAACH5BAAAAP8ALAAAAAANAA0AAAQnMMhJqbg4C6k77wJwfRogbsF1huaYsuz6mXRG
+1varejqI+piKcBIBADs=
+--------------080903080202020605050108--
new file mode 100644
--- /dev/null
+++ b/mailnews/test/data/image-attach-test
@@ -0,0 +1,63 @@
+From - Fri May 20 16:47:38 2008
+X-Identity-Key: id2
+Message-ID: <4A947F73.5030709@xxx.com>
+Date: Tue, 25 Aug 2009 17:18:59 -0700
+From: Foo Bar <xxx@xxx.com>
+Reply-To: xxx@xxx.com
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.3pre) Gecko/20090817 Shredder/3.0b4pre
+MIME-Version: 1.0
+To: asfd@asdf
+Subject: image attach test
+Content-Type: multipart/mixed;
+ boundary="------------080903080202020605050108"
+
+This is a multi-part message in MIME format.
+--------------080903080202020605050108
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+
+<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <title>image attach test</title>
+</head>
+<body bgcolor="#ffffff" text="#000000">
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890<br>
+test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890test
+1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
+</body>
+</html>
+
+--------------080903080202020605050108
+Content-Type: image/gif;
+ name="check.gif"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="check.gif"
+
+R0lGODlhDQANAIMAAAAAAICAgP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAACH5BAAAAP8ALAAAAAANAA0AAAQnMMhJqbg4C6k77wJwfRogbsF1huaYsuz6mXRG
+1varejqI+piKcBIBADs=
+--------------080903080202020605050108--